diff --git a/.vscode/launch.json b/.vscode/launch.json index 930f764b9b190..1c3f45063cf79 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -81,12 +81,26 @@ { "type": "extensionHost", "request": "launch", - "name": "VS Code API Tests", + "name": "VS Code API Tests (single folder)", "runtimeExecutable": "${execPath}", "args": [ "${workspaceFolder}/extensions/vscode-api-tests/testWorkspace", "--extensionDevelopmentPath=${workspaceFolder}/extensions/vscode-api-tests", - "--extensionTestsPath=${workspaceFolder}/extensions/vscode-api-tests/out" + "--extensionTestsPath=${workspaceFolder}/extensions/vscode-api-tests/out/singlefolder-tests" + ], + "outFiles": [ + "${workspaceFolder}/out/**/*.js" + ] + }, + { + "type": "extensionHost", + "request": "launch", + "name": "VS Code API Tests (workspace)", + "runtimeExecutable": "${execPath}", + "args": [ + "${workspaceFolder}/extensions/vscode-api-tests/testWorkspace.code-workspace", + "--extensionDevelopmentPath=${workspaceFolder}/extensions/vscode-api-tests", + "--extensionTestsPath=${workspaceFolder}/extensions/vscode-api-tests/out/workspace-tests" ], "outFiles": [ "${workspaceFolder}/out/**/*.js" diff --git a/build/builtInExtensions.json b/build/builtInExtensions.json index 034d02fcbbe47..8e4aded2a2883 100644 --- a/build/builtInExtensions.json +++ b/build/builtInExtensions.json @@ -1,7 +1,7 @@ [ { "name": "ms-vscode.node-debug", - "version": "1.20.4", + "version": "1.20.6", "repo": "https://github.com/Microsoft/vscode-node-debug" }, { diff --git a/build/gulpfile.hygiene.js b/build/gulpfile.hygiene.js index f766cfaca0f5d..d6402600d27c5 100644 --- a/build/gulpfile.hygiene.js +++ b/build/gulpfile.hygiene.js @@ -77,7 +77,8 @@ const indentationFilter = [ '!extensions/**/syntaxes/**', '!extensions/**/themes/**', '!extensions/**/colorize-fixtures/**', - '!extensions/vscode-api-tests/testWorkspace/**' + '!extensions/vscode-api-tests/testWorkspace/**', + '!extensions/vscode-api-tests/testWorkspace2/**' ]; const copyrightFilter = [ @@ -95,6 +96,7 @@ const copyrightFilter = [ '!**/*.xpm', '!**/*.opts', '!**/*.disabled', + '!**/*.code-workspace', '!build/**/*.init', '!resources/linux/snap/snapcraft.yaml', '!resources/win32/bin/code.js', @@ -124,6 +126,7 @@ const tslintFilter = [ '!**/node_modules/**', '!extensions/typescript/test/colorize-fixtures/**', '!extensions/vscode-api-tests/testWorkspace/**', + '!extensions/vscode-api-tests/testWorkspace2/**', '!extensions/**/*.test.ts', '!extensions/html/server/lib/jquery.d.ts' ]; diff --git a/build/gulpfile.vscode.js b/build/gulpfile.vscode.js index 1dd0aca338bb6..66a71634ce3ac 100644 --- a/build/gulpfile.vscode.js +++ b/build/gulpfile.vscode.js @@ -401,7 +401,7 @@ const apiHostname = process.env.TRANSIFEX_API_URL; const apiName = process.env.TRANSIFEX_API_NAME; const apiToken = process.env.TRANSIFEX_API_TOKEN; -gulp.task('vscode-translations-push', ['optimize-vscode'], function () { +gulp.task('vscode-translations-push', function () { const pathToMetadata = './out-vscode/nls.metadata.json'; const pathToExtensions = './extensions/*'; const pathToSetup = 'build/win32/**/{Default.isl,messages.en.isl}'; @@ -410,6 +410,7 @@ gulp.task('vscode-translations-push', ['optimize-vscode'], function () { gulp.src(pathToMetadata).pipe(i18n.createXlfFilesForCoreBundle()), gulp.src(pathToSetup).pipe(i18n.createXlfFilesForIsl()), gulp.src(pathToExtensions).pipe(i18n.createXlfFilesForExtensions()) + ).pipe(i18n.findObsoleteResources(apiHostname, apiName, apiToken) ).pipe(i18n.pushXlfFiles(apiHostname, apiName, apiToken)); }); @@ -422,12 +423,13 @@ gulp.task('vscode-translations-push-test', function () { gulp.src(pathToMetadata).pipe(i18n.createXlfFilesForCoreBundle()), gulp.src(pathToSetup).pipe(i18n.createXlfFilesForIsl()), gulp.src(pathToExtensions).pipe(i18n.createXlfFilesForExtensions()) + ).pipe(i18n.findObsoleteResources(apiHostname, apiName, apiToken) ).pipe(vfs.dest('../vscode-transifex-input')); }); gulp.task('vscode-translations-pull', function () { [...i18n.defaultLanguages, ...i18n.extraLanguages].forEach(language => { - i18n.pullBuildXlfFiles(apiHostname, apiName, apiToken, language).pipe(vfs.dest(`../vscode-localization/${language.id}/build`)); + i18n.pullCoreAndExtensionsXlfFiles(apiHostname, apiName, apiToken, language).pipe(vfs.dest(`../vscode-localization/${language.id}/build`)); let includeDefault = !!innoSetupConfig[language.id].defaultInfo; i18n.pullSetupXlfFiles(apiHostname, apiName, apiToken, language, includeDefault).pipe(vfs.dest(`../vscode-localization/${language.id}/setup`)); diff --git a/build/lib/i18n.js b/build/lib/i18n.js index 907010f0841c5..b77dc91a0d50e 100644 --- a/build/lib/i18n.js +++ b/build/lib/i18n.js @@ -15,7 +15,7 @@ var https = require("https"); var gulp = require("gulp"); var util = require('gulp-util'); var iconv = require('iconv-lite'); -var NUMBER_OF_CONCURRENT_DOWNLOADS = 1; +var NUMBER_OF_CONCURRENT_DOWNLOADS = 4; function log(message) { var rest = []; for (var _i = 1; _i < arguments.length; _i++) { @@ -41,6 +41,14 @@ exports.extraLanguages = [ { id: 'tr', folderName: 'trk' } ]; exports.pseudoLanguage = { id: 'pseudo', folderName: 'pseudo', transifexId: 'pseudo' }; +// non built-in extensions also that are transifex and need to be part of the language packs +var externalExtensionsWithTranslations = [ + //"azure-account", + "vscode-chrome-debug", + "vscode-chrome-debug-core", + "vscode-node-debug", + "vscode-node-debug2" +]; var LocalizeInfo; (function (LocalizeInfo) { function is(value) { @@ -121,6 +129,7 @@ var XLF = /** @class */ (function () { this.project = project; this.buffer = []; this.files = Object.create(null); + this.numberOfMessages = 0; } XLF.prototype.toString = function () { this.appendHeader(); @@ -139,6 +148,7 @@ var XLF = /** @class */ (function () { if (keys.length !== messages.length) { throw new Error("Unmatching keys(" + keys.length + ") and messages(" + messages.length + ")."); } + this.numberOfMessages += keys.length; this.files[original] = []; var existingKeys = new Set(); for (var i = 0; i < keys.length; i++) { @@ -701,6 +711,88 @@ function pushXlfFiles(apiHostname, username, password) { }); } exports.pushXlfFiles = pushXlfFiles; +function getAllResources(project, apiHostname, username, password) { + return new Promise(function (resolve, reject) { + var credentials = username + ":" + password; + var options = { + hostname: apiHostname, + path: "/api/2/project/" + project + "/resources", + auth: credentials, + method: 'GET' + }; + var request = https.request(options, function (res) { + var buffer = []; + res.on('data', function (chunk) { return buffer.push(chunk); }); + res.on('end', function () { + if (res.statusCode === 200) { + var json = JSON.parse(Buffer.concat(buffer).toString()); + if (Array.isArray(json)) { + resolve(json.map(function (o) { return o.slug; })); + return; + } + reject("Unexpected data format. Response code: " + res.statusCode + "."); + } + else { + reject("No resources in " + project + " returned no data. Response code: " + res.statusCode + "."); + } + }); + }); + request.on('error', function (err) { + reject("Failed to query resources in " + project + " with the following error: " + err + ". " + options.path); + }); + request.end(); + }); +} +function findObsoleteResources(apiHostname, username, password) { + var resourcesByProject = Object.create(null); + resourcesByProject[extensionsProject] = [].concat(externalExtensionsWithTranslations); // clone + return event_stream_1.through(function (file) { + var project = path.dirname(file.relative); + var fileName = path.basename(file.path); + var slug = fileName.substr(0, fileName.length - '.xlf'.length); + var slugs = resourcesByProject[project]; + if (!slugs) { + resourcesByProject[project] = slugs = []; + } + slugs.push(slug); + this.push(file); + }, function () { + var _this = this; + var json = JSON.parse(fs.readFileSync('./build/lib/i18n.resources.json', 'utf8')); + var i18Resources = json.editor.concat(json.workbench).map(function (r) { return r.project + '/' + r.name.replace(/\//g, '_'); }); + var extractedResources = []; + for (var _i = 0, _a = [workbenchProject, editorProject]; _i < _a.length; _i++) { + var project = _a[_i]; + for (var _b = 0, _c = resourcesByProject[project]; _b < _c.length; _b++) { + var resource = _c[_b]; + if (resource !== 'setup_messages') { + extractedResources.push(project + '/' + resource); + } + } + } + if (i18Resources.length !== extractedResources.length) { + console.log("[i18n] Obsolete resources in file 'build/lib/i18n.resources.json': JSON.stringify(" + i18Resources.filter(function (p) { return extractedResources.indexOf(p) === -1; }) + ")"); + console.log("[i18n] Missing resources in file 'build/lib/i18n.resources.json': JSON.stringify(" + extractedResources.filter(function (p) { return i18Resources.indexOf(p) === -1; }) + ")"); + } + var promises = []; + var _loop_1 = function (project) { + promises.push(getAllResources(project, apiHostname, username, password).then(function (resources) { + var expectedResources = resourcesByProject[project]; + var unusedResources = resources.filter(function (resource) { return resource && expectedResources.indexOf(resource) === -1; }); + if (unusedResources.length) { + console.log("[transifex] Obsolete resources in project '" + project + "': " + unusedResources.join(', ')); + } + })); + }; + for (var project in resourcesByProject) { + _loop_1(project); + } + return Promise.all(promises).then(function (_) { + _this.push(null); + }).catch(function (reason) { throw new Error(reason); }); + }); +} +exports.findObsoleteResources = findObsoleteResources; function tryGetResource(project, slug, apiHostname, credentials) { return new Promise(function (resolve, reject) { var options = { @@ -801,29 +893,35 @@ function updateResource(project, slug, xlfFile, apiHostname, credentials) { }); } // cache resources -var _buildResources; -function pullBuildXlfFiles(apiHostname, username, password, language) { - if (!_buildResources) { - _buildResources = []; +var _coreAndExtensionResources; +function pullCoreAndExtensionsXlfFiles(apiHostname, username, password, language, includeExternalExtensions) { + if (!_coreAndExtensionResources) { + _coreAndExtensionResources = []; // editor and workbench var json = JSON.parse(fs.readFileSync('./build/lib/i18n.resources.json', 'utf8')); - _buildResources.push.apply(_buildResources, json.editor); - _buildResources.push.apply(_buildResources, json.workbench); + _coreAndExtensionResources.push.apply(_coreAndExtensionResources, json.editor); + _coreAndExtensionResources.push.apply(_coreAndExtensionResources, json.workbench); // extensions var extensionsToLocalize_1 = Object.create(null); glob.sync('./extensions/**/*.nls.json').forEach(function (extension) { return extensionsToLocalize_1[extension.split('/')[2]] = true; }); glob.sync('./extensions/*/node_modules/vscode-nls').forEach(function (extension) { return extensionsToLocalize_1[extension.split('/')[2]] = true; }); + if (includeExternalExtensions) { + for (var _i = 0, externalExtensionsWithTranslations_1 = externalExtensionsWithTranslations; _i < externalExtensionsWithTranslations_1.length; _i++) { + var extension = externalExtensionsWithTranslations_1[_i]; + extensionsToLocalize_1[extension] = true; + } + } Object.keys(extensionsToLocalize_1).forEach(function (extension) { - _buildResources.push({ name: extension, project: 'vscode-extensions' }); + _coreAndExtensionResources.push({ name: extension, project: extensionsProject }); }); } - return pullXlfFiles(apiHostname, username, password, language, _buildResources); + return pullXlfFiles(apiHostname, username, password, language, _coreAndExtensionResources); } -exports.pullBuildXlfFiles = pullBuildXlfFiles; +exports.pullCoreAndExtensionsXlfFiles = pullCoreAndExtensionsXlfFiles; function pullSetupXlfFiles(apiHostname, username, password, language, includeDefault) { - var setupResources = [{ name: 'setup_messages', project: 'vscode-workbench' }]; + var setupResources = [{ name: 'setup_messages', project: workbenchProject }]; if (includeDefault) { - setupResources.push({ name: 'setup_default', project: 'vscode-setup' }); + setupResources.push({ name: 'setup_default', project: setupProject }); } return pullXlfFiles(apiHostname, username, password, language, setupResources); } @@ -865,6 +963,7 @@ function retrieveResource(language, resource, apiHostname, credentials) { port: 443, method: 'GET' }; + console.log('Fetching ' + options.path); var request = https.request(options, function (res) { var xlfBuffer = []; res.on('data', function (chunk) { return xlfBuffer.push(chunk); }); @@ -928,7 +1027,7 @@ function createI18nFile(originalFilePath, messages) { } var i18nPackVersion = "1.0.0"; function pullI18nPackFiles(apiHostname, username, password, language) { - return pullBuildXlfFiles(apiHostname, username, password, language).pipe(prepareI18nPackFiles()); + return pullCoreAndExtensionsXlfFiles(apiHostname, username, password, language, true).pipe(prepareI18nPackFiles()); } exports.pullI18nPackFiles = pullI18nPackFiles; function prepareI18nPackFiles() { @@ -937,32 +1036,26 @@ function prepareI18nPackFiles() { var extensionsPacks = {}; return event_stream_1.through(function (xlf) { var stream = this; + var project = path.dirname(xlf.path); + var resource = path.basename(xlf.path, '.xlf'); + console.log(resource); var parsePromise = XLF.parse(xlf.contents.toString()); parsePromises.push(parsePromise); parsePromise.then(function (resolvedFiles) { resolvedFiles.forEach(function (file) { var path = file.originalFilePath; var firstSlash = path.indexOf('/'); - var firstSegment = path.substr(0, firstSlash); - if (firstSegment === 'src') { - mainPack.contents[path.substr(firstSlash + 1)] = file.messages; - } - else if (firstSegment === 'extensions') { - var secondSlash = path.indexOf('/', firstSlash + 1); - var secondSegment = path.substring(firstSlash + 1, secondSlash); - if (secondSegment) { - var extPack = extensionsPacks[secondSegment]; - if (!extPack) { - extPack = extensionsPacks[secondSegment] = { version: i18nPackVersion, contents: {} }; - } - extPack.contents[path.substr(secondSlash + 1)] = file.messages; - } - else { - console.log('Unknown second segment ' + path); + if (project === extensionsProject) { + var extPack = extensionsPacks[resource]; + if (!extPack) { + extPack = extensionsPacks[resource] = { version: i18nPackVersion, contents: {} }; } + var secondSlash = path.indexOf('/', firstSlash + 1); + var key = externalExtensionsWithTranslations.indexOf(resource) !== -1 ? path : path.substr(secondSlash + 1); + extPack.contents[key] = file.messages; } else { - console.log('Unknown first segment ' + path); + mainPack.contents[path.substr(firstSlash + 1)] = file.messages; } }); }); diff --git a/build/lib/i18n.resources.json b/build/lib/i18n.resources.json index 499ef117f7dd1..909ab84d057cb 100644 --- a/build/lib/i18n.resources.json +++ b/build/lib/i18n.resources.json @@ -46,10 +46,6 @@ "name": "vs/workbench/parts/execution", "project": "vscode-workbench" }, - { - "name": "vs/workbench/parts/explorers", - "project": "vscode-workbench" - }, { "name": "vs/workbench/parts/extensions", "project": "vscode-workbench" @@ -74,10 +70,6 @@ "name": "vs/workbench/parts/logs", "project": "vscode-workbench" }, - { - "name": "vs/workbench/parts/nps", - "project": "vscode-workbench" - }, { "name": "vs/workbench/parts/output", "project": "vscode-workbench" diff --git a/build/lib/i18n.ts b/build/lib/i18n.ts index aa249e1f2a34a..672bd18981681 100644 --- a/build/lib/i18n.ts +++ b/build/lib/i18n.ts @@ -17,7 +17,7 @@ import * as gulp from 'gulp'; var util = require('gulp-util'); var iconv = require('iconv-lite'); -const NUMBER_OF_CONCURRENT_DOWNLOADS = 1; +const NUMBER_OF_CONCURRENT_DOWNLOADS = 4; function log(message: any, ...rest: any[]): void { util.log(util.colors.green('[i18n]'), message, ...rest); @@ -58,6 +58,15 @@ export const extraLanguages: Language[] = [ export const pseudoLanguage: Language = { id: 'pseudo', folderName: 'pseudo', transifexId: 'pseudo' }; +// non built-in extensions also that are transifex and need to be part of the language packs +const externalExtensionsWithTranslations = [ + //"azure-account", + "vscode-chrome-debug", + "vscode-chrome-debug-core", + "vscode-node-debug", + "vscode-node-debug2" +]; + interface Map { [key: string]: V; } @@ -193,10 +202,12 @@ class TextModel { export class XLF { private buffer: string[]; private files: Map; + public numberOfMessages: number; constructor(public project: string) { this.buffer = []; this.files = Object.create(null); + this.numberOfMessages = 0; } public toString(): string { @@ -218,6 +229,7 @@ export class XLF { if (keys.length !== messages.length) { throw new Error(`Unmatching keys(${keys.length}) and messages(${messages.length}).`); } + this.numberOfMessages += keys.length; this.files[original] = []; let existingKeys = new Set(); for (let i = 0; i < keys.length; i++) { @@ -808,6 +820,89 @@ export function pushXlfFiles(apiHostname: string, username: string, password: st }); } +function getAllResources(project: string, apiHostname: string, username: string, password: string): Promise { + return new Promise((resolve, reject) => { + const credentials = `${username}:${password}`; + const options = { + hostname: apiHostname, + path: `/api/2/project/${project}/resources`, + auth: credentials, + method: 'GET' + }; + + const request = https.request(options, (res) => { + let buffer: Buffer[] = []; + res.on('data', (chunk: Buffer) => buffer.push(chunk)); + res.on('end', () => { + if (res.statusCode === 200) { + let json = JSON.parse(Buffer.concat(buffer).toString()); + if (Array.isArray(json)) { + resolve(json.map(o => o.slug)); + return; + } + reject(`Unexpected data format. Response code: ${res.statusCode}.`); + } else { + reject(`No resources in ${project} returned no data. Response code: ${res.statusCode}.`); + } + }); + }); + request.on('error', (err) => { + reject(`Failed to query resources in ${project} with the following error: ${err}. ${options.path}`); + }); + request.end(); + }); +} + +export function findObsoleteResources(apiHostname: string, username: string, password: string): ThroughStream { + let resourcesByProject: Map = Object.create(null); + resourcesByProject[extensionsProject] = [].concat(externalExtensionsWithTranslations); // clone + + return through(function (this: ThroughStream, file: File) { + const project = path.dirname(file.relative); + const fileName = path.basename(file.path); + const slug = fileName.substr(0, fileName.length - '.xlf'.length); + + let slugs = resourcesByProject[project]; + if (!slugs) { + resourcesByProject[project] = slugs = []; + } + slugs.push(slug); + this.push(file); + }, function () { + + const json = JSON.parse(fs.readFileSync('./build/lib/i18n.resources.json', 'utf8')); + let i18Resources = [...json.editor, ...json.workbench].map((r: Resource) => r.project + '/' + r.name.replace(/\//g, '_')); + let extractedResources = []; + for (let project of [workbenchProject, editorProject]) { + for (let resource of resourcesByProject[project]) { + if (resource !== 'setup_messages') { + extractedResources.push(project + '/' + resource); + } + } + } + if (i18Resources.length !== extractedResources.length) { + console.log(`[i18n] Obsolete resources in file 'build/lib/i18n.resources.json': JSON.stringify(${i18Resources.filter(p => extractedResources.indexOf(p) === -1)})`); + console.log(`[i18n] Missing resources in file 'build/lib/i18n.resources.json': JSON.stringify(${extractedResources.filter(p => i18Resources.indexOf(p) === -1)})`); + } + + let promises = []; + for (let project in resourcesByProject) { + promises.push( + getAllResources(project, apiHostname, username, password).then(resources => { + let expectedResources = resourcesByProject[project]; + let unusedResources = resources.filter(resource => resource && expectedResources.indexOf(resource) === -1); + if (unusedResources.length) { + console.log(`[transifex] Obsolete resources in project '${project}': ${unusedResources.join(', ')}`); + } + }) + ); + } + return Promise.all(promises).then(_ => { + this.push(null); + }).catch((reason) => { throw new Error(reason); }); + }); +} + function tryGetResource(project: string, slug: string, apiHostname: string, credentials: string): Promise { return new Promise((resolve, reject) => { const options = { @@ -914,32 +1009,38 @@ function updateResource(project: string, slug: string, xlfFile: File, apiHostnam } // cache resources -let _buildResources: Resource[]; +let _coreAndExtensionResources: Resource[]; -export function pullBuildXlfFiles(apiHostname: string, username: string, password: string, language: Language): NodeJS.ReadableStream { - if (!_buildResources) { - _buildResources = []; +export function pullCoreAndExtensionsXlfFiles(apiHostname: string, username: string, password: string, language: Language, includeExternalExtensions?: boolean): NodeJS.ReadableStream { + if (!_coreAndExtensionResources) { + _coreAndExtensionResources = []; // editor and workbench const json = JSON.parse(fs.readFileSync('./build/lib/i18n.resources.json', 'utf8')); - _buildResources.push(...json.editor); - _buildResources.push(...json.workbench); + _coreAndExtensionResources.push(...json.editor); + _coreAndExtensionResources.push(...json.workbench); // extensions let extensionsToLocalize = Object.create(null); glob.sync('./extensions/**/*.nls.json', ).forEach(extension => extensionsToLocalize[extension.split('/')[2]] = true); glob.sync('./extensions/*/node_modules/vscode-nls', ).forEach(extension => extensionsToLocalize[extension.split('/')[2]] = true); + if (includeExternalExtensions) { + for (let extension of externalExtensionsWithTranslations) { + extensionsToLocalize[extension] = true; + } + } + Object.keys(extensionsToLocalize).forEach(extension => { - _buildResources.push({ name: extension, project: 'vscode-extensions' }); + _coreAndExtensionResources.push({ name: extension, project: extensionsProject }); }); } - return pullXlfFiles(apiHostname, username, password, language, _buildResources); + return pullXlfFiles(apiHostname, username, password, language, _coreAndExtensionResources); } export function pullSetupXlfFiles(apiHostname: string, username: string, password: string, language: Language, includeDefault: boolean): NodeJS.ReadableStream { - let setupResources = [{ name: 'setup_messages', project: 'vscode-workbench' }]; + let setupResources = [{ name: 'setup_messages', project: workbenchProject }]; if (includeDefault) { - setupResources.push({ name: 'setup_default', project: 'vscode-setup' }); + setupResources.push({ name: 'setup_default', project: setupProject }); } return pullXlfFiles(apiHostname, username, password, language, setupResources); } @@ -985,6 +1086,7 @@ function retrieveResource(language: Language, resource: Resource, apiHostname, c port: 443, method: 'GET' }; + console.log('Fetching ' + options.path); let request = https.request(options, (res) => { let xlfBuffer: Buffer[] = []; @@ -1059,7 +1161,7 @@ interface I18nPack { const i18nPackVersion = "1.0.0"; export function pullI18nPackFiles(apiHostname: string, username: string, password: string, language: Language): NodeJS.ReadableStream { - return pullBuildXlfFiles(apiHostname, username, password, language).pipe(prepareI18nPackFiles()); + return pullCoreAndExtensionsXlfFiles(apiHostname, username, password, language, true).pipe(prepareI18nPackFiles()); } export function prepareI18nPackFiles() { @@ -1068,6 +1170,9 @@ export function prepareI18nPackFiles() { let extensionsPacks: Map = {}; return through(function (this: ThroughStream, xlf: File) { let stream = this; + let project = path.dirname(xlf.path); + let resource = path.basename(xlf.path, '.xlf'); + console.log(resource); let parsePromise = XLF.parse(xlf.contents.toString()); parsePromises.push(parsePromise); parsePromise.then( @@ -1075,23 +1180,17 @@ export function prepareI18nPackFiles() { resolvedFiles.forEach(file => { const path = file.originalFilePath; const firstSlash = path.indexOf('/'); - const firstSegment = path.substr(0, firstSlash); - if (firstSegment === 'src') { - mainPack.contents[path.substr(firstSlash + 1)] = file.messages; - } else if (firstSegment === 'extensions') { - const secondSlash = path.indexOf('/', firstSlash + 1); - const secondSegment = path.substring(firstSlash + 1, secondSlash); - if (secondSegment) { - let extPack = extensionsPacks[secondSegment]; - if (!extPack) { - extPack = extensionsPacks[secondSegment] = { version: i18nPackVersion, contents: {} }; - } - extPack.contents[path.substr(secondSlash + 1)] = file.messages; - } else { - console.log('Unknown second segment ' + path); + + if (project === extensionsProject) { + let extPack = extensionsPacks[resource]; + if (!extPack) { + extPack = extensionsPacks[resource] = { version: i18nPackVersion, contents: {} }; } + const secondSlash = path.indexOf('/', firstSlash + 1); + let key = externalExtensionsWithTranslations.indexOf(resource) !== -1 ? path: path.substr(secondSlash + 1); + extPack.contents[key] = file.messages; } else { - console.log('Unknown first segment ' + path); + mainPack.contents[path.substr(firstSlash + 1)] = file.messages; } }); } diff --git a/build/npm/update-localization-extension.js b/build/npm/update-localization-extension.js index ca8a4139d6d25..bcdaa56d304ef 100644 --- a/build/npm/update-localization-extension.js +++ b/build/npm/update-localization-extension.js @@ -19,7 +19,7 @@ function update(idOrPath) { } let locExtFolder = idOrPath; if (/^\w{2}(-\w+)?$/.test(idOrPath)) { - locExtFolder = '../vscode-localization-' + idOrPath; + locExtFolder = '../vscode-language-pack-' + idOrPath; } let locExtStat = fs.statSync(locExtFolder); if (!locExtStat || !locExtStat.isDirectory) { diff --git a/extensions/configuration-editing/src/settingsDocumentHelper.ts b/extensions/configuration-editing/src/settingsDocumentHelper.ts index 5f9cc61c9a906..73de7fef8a876 100644 --- a/extensions/configuration-editing/src/settingsDocumentHelper.ts +++ b/extensions/configuration-editing/src/settingsDocumentHelper.ts @@ -60,26 +60,26 @@ export class SettingsDocument { private provideFilesAssociationsCompletionItems(location: Location, range: vscode.Range): vscode.ProviderResult { const completions: vscode.CompletionItem[] = []; - // Key - if (location.path.length === 1) { - completions.push(this.newSnippetCompletionItem({ - label: localize('assocLabelFile', "Files with Extension"), - documentation: localize('assocDescriptionFile', "Map all files matching the glob pattern in their filename to the language with the given identifier."), - snippet: location.isAtPropertyKey ? '"*.${1:extension}": "${2:language}"' : '{ "*.${1:extension}": "${2:language}" }', - range - })); - - completions.push(this.newSnippetCompletionItem({ - label: localize('assocLabelPath', "Files with Path"), - documentation: localize('assocDescriptionPath', "Map all files matching the absolute path glob pattern in their path to the language with the given identifier."), - snippet: location.isAtPropertyKey ? '"/${1:path to file}/*.${2:extension}": "${3:language}"' : '{ "/${1:path to file}/*.${2:extension}": "${3:language}" }', - range - })); - } - - // Value - else if (location.path.length === 2 && !location.isAtPropertyKey) { - return this.provideLanguageCompletionItems(location, range); + if (location.path.length === 2) { + // Key + if (!location.isAtPropertyKey || location.path[1] === '') { + completions.push(this.newSnippetCompletionItem({ + label: localize('assocLabelFile', "Files with Extension"), + documentation: localize('assocDescriptionFile', "Map all files matching the glob pattern in their filename to the language with the given identifier."), + snippet: location.isAtPropertyKey ? '"*.${1:extension}": "${2:language}"' : '{ "*.${1:extension}": "${2:language}" }', + range + })); + + completions.push(this.newSnippetCompletionItem({ + label: localize('assocLabelPath', "Files with Path"), + documentation: localize('assocDescriptionPath', "Map all files matching the absolute path glob pattern in their path to the language with the given identifier."), + snippet: location.isAtPropertyKey ? '"/${1:path to file}/*.${2:extension}": "${3:language}"' : '{ "/${1:path to file}/*.${2:extension}": "${3:language}" }', + range + })); + } else { + // Value + return this.provideLanguageCompletionItems(location, range); + } } return Promise.resolve(completions); diff --git a/extensions/git/package.json b/extensions/git/package.json index e1c8322553edb..5834266c26e20 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -726,7 +726,7 @@ }, { "command": "git.openFile2", - "when": "scmProvider == git && scmResourceGroup == merge", + "when": "scmProvider == git && scmResourceGroup == merge && config.git.showInlineOpenFileAction", "group": "inline0" }, { @@ -756,7 +756,7 @@ }, { "command": "git.openFile2", - "when": "scmProvider == git && scmResourceGroup == index", + "when": "scmProvider == git && scmResourceGroup == index && config.git.showInlineOpenFileAction", "group": "inline0" }, { @@ -796,7 +796,7 @@ }, { "command": "git.openFile2", - "when": "scmProvider == git && scmResourceGroup == workingTree", + "when": "scmProvider == git && scmResourceGroup == workingTree && config.git.showInlineOpenFileAction", "group": "inline0" }, { @@ -936,6 +936,16 @@ "type": "boolean", "default": true, "description": "%config.decorations.enabled%" + }, + "git.promptToSaveFilesBeforeCommit": { + "type": "boolean", + "default": false, + "description": "%config.promptToSaveFilesBeforeCommit%" + }, + "git.showInlineOpenFileAction": { + "type": "boolean", + "default": true, + "description": "%config.showInlineOpenFileAction%" } } }, @@ -1012,4 +1022,4 @@ "@types/which": "^1.0.28", "mocha": "^3.2.0" } -} +} \ No newline at end of file diff --git a/extensions/git/package.nls.json b/extensions/git/package.nls.json index 355d8b07a6367..28c41092842eb 100644 --- a/extensions/git/package.nls.json +++ b/extensions/git/package.nls.json @@ -64,10 +64,12 @@ "config.enableCommitSigning": "Enables commit signing with GPG.", "config.discardAllScope": "Controls what changes are discarded by the `Discard all changes` command. `all` discards all changes. `tracked` discards only tracked files. `prompt` shows a prompt dialog every time the action is run.", "config.decorations.enabled": "Controls if Git contributes colors and badges to the explorer and the open editors view.", + "config.promptToSaveFilesBeforeCommit": "Controls whether Git should check for unsaved files before committing.", + "config.showInlineOpenFileAction": "Controls whether to show an inline Open File action in the Git changes view.", "colors.modified": "Color for modified resources.", "colors.deleted": "Color for deleted resources.", "colors.untracked": "Color for untracked resources.", "colors.ignored": "Color for ignored resources.", "colors.conflict": "Color for resources with conflicts.", "colors.submodule": "Color for submodule resources." -} +} \ No newline at end of file diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index 84a011a8881b6..776e6c8ac1b4f 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -963,26 +963,30 @@ export class CommandCenter { getCommitMessage: () => Promise, opts?: CommitOptions ): Promise { - const unsavedTextDocuments = workspace.textDocuments - .filter(d => !d.isUntitled && d.isDirty && isDescendant(repository.root, d.uri.fsPath)); - - if (unsavedTextDocuments.length > 0) { - const message = unsavedTextDocuments.length === 1 - ? localize('unsaved files single', "The following file is unsaved: {0}.\n\nWould you like to save it before comitting?", path.basename(unsavedTextDocuments[0].uri.fsPath)) - : localize('unsaved files', "There are {0} unsaved files.\n\nWould you like to save them before comitting?", unsavedTextDocuments.length); - const saveAndCommit = localize('save and commit', "Save All & Commit"); - const commit = localize('commit', "Commit Anyway"); - const pick = await window.showWarningMessage(message, { modal: true }, saveAndCommit, commit); - - if (pick === saveAndCommit) { - await Promise.all(unsavedTextDocuments.map(d => d.save())); - await repository.status(); - } else if (pick !== commit) { - return false; // do not commit on cancel + const config = workspace.getConfiguration('git'); + const promptToSaveFilesBeforeCommit = config.get('promptToSaveFilesBeforeCommit') === true; + + if (promptToSaveFilesBeforeCommit) { + const unsavedTextDocuments = workspace.textDocuments + .filter(d => !d.isUntitled && d.isDirty && isDescendant(repository.root, d.uri.fsPath)); + + if (unsavedTextDocuments.length > 0) { + const message = unsavedTextDocuments.length === 1 + ? localize('unsaved files single', "The following file is unsaved: {0}.\n\nWould you like to save it before comitting?", path.basename(unsavedTextDocuments[0].uri.fsPath)) + : localize('unsaved files', "There are {0} unsaved files.\n\nWould you like to save them before comitting?", unsavedTextDocuments.length); + const saveAndCommit = localize('save and commit', "Save All & Commit"); + const commit = localize('commit', "Commit Anyway"); + const pick = await window.showWarningMessage(message, { modal: true }, saveAndCommit, commit); + + if (pick === saveAndCommit) { + await Promise.all(unsavedTextDocuments.map(d => d.save())); + await repository.status(); + } else if (pick !== commit) { + return false; // do not commit on cancel + } } } - const config = workspace.getConfiguration('git'); const enableSmartCommit = config.get('enableSmartCommit') === true; const enableCommitSigning = config.get('enableCommitSigning') === true; const noStagedChanges = repository.indexGroup.resourceStates.length === 0; diff --git a/extensions/vscode-api-tests/src/commands.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/commands.test.ts similarity index 100% rename from extensions/vscode-api-tests/src/commands.test.ts rename to extensions/vscode-api-tests/src/singlefolder-tests/commands.test.ts diff --git a/extensions/vscode-api-tests/src/configuration.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/configuration.test.ts similarity index 100% rename from extensions/vscode-api-tests/src/configuration.test.ts rename to extensions/vscode-api-tests/src/singlefolder-tests/configuration.test.ts diff --git a/extensions/vscode-api-tests/src/editor.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/editor.test.ts similarity index 99% rename from extensions/vscode-api-tests/src/editor.test.ts rename to extensions/vscode-api-tests/src/singlefolder-tests/editor.test.ts index a5d597b4bc0db..0743a5e0cb3ef 100644 --- a/extensions/vscode-api-tests/src/editor.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/editor.test.ts @@ -7,7 +7,7 @@ import * as assert from 'assert'; import { workspace, window, Position, Range, commands, TextEditor, TextDocument, TextEditorCursorStyle, TextEditorLineNumbersStyle, SnippetString, Selection } from 'vscode'; -import { createRandomFile, deleteFile, closeAllEditors } from './utils'; +import { createRandomFile, deleteFile, closeAllEditors } from '../utils'; suite('editor tests', () => { diff --git a/extensions/vscode-api-tests/src/env.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/env.test.ts similarity index 100% rename from extensions/vscode-api-tests/src/env.test.ts rename to extensions/vscode-api-tests/src/singlefolder-tests/env.test.ts diff --git a/extensions/vscode-api-tests/src/index.ts b/extensions/vscode-api-tests/src/singlefolder-tests/index.ts similarity index 100% rename from extensions/vscode-api-tests/src/index.ts rename to extensions/vscode-api-tests/src/singlefolder-tests/index.ts diff --git a/extensions/vscode-api-tests/src/languages.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/languages.test.ts similarity index 99% rename from extensions/vscode-api-tests/src/languages.test.ts rename to extensions/vscode-api-tests/src/singlefolder-tests/languages.test.ts index 01dea3ddc3f29..e385d5287fdf4 100644 --- a/extensions/vscode-api-tests/src/languages.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/languages.test.ts @@ -21,7 +21,7 @@ suite('languages namespace tests', () => { constructor() { super(new Range(0, 2, 0, 7), 'sonntag'); } - }; + } let diag1 = new Diagnostic(new Range(0, 0, 0, 5), 'montag'); let diag2 = new D2(); diff --git a/extensions/vscode-api-tests/src/window.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/window.test.ts similarity index 99% rename from extensions/vscode-api-tests/src/window.test.ts rename to extensions/vscode-api-tests/src/singlefolder-tests/window.test.ts index 2a2f92c90e748..97bace17e732c 100644 --- a/extensions/vscode-api-tests/src/window.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/window.test.ts @@ -8,7 +8,7 @@ import * as assert from 'assert'; import { workspace, window, commands, ViewColumn, TextEditorViewColumnChangeEvent, Uri, Selection, Position, CancellationTokenSource, TextEditorSelectionChangeKind } from 'vscode'; import { join } from 'path'; -import { closeAllEditors, pathEquals, createRandomFile } from './utils'; +import { closeAllEditors, pathEquals, createRandomFile } from '../utils'; suite('window namespace tests', () => { diff --git a/extensions/vscode-api-tests/src/workspace.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts similarity index 96% rename from extensions/vscode-api-tests/src/workspace.test.ts rename to extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts index d698bba1eabe0..87ec01ef07255 100644 --- a/extensions/vscode-api-tests/src/workspace.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts @@ -7,10 +7,9 @@ import * as assert from 'assert'; import * as vscode from 'vscode'; -import { createRandomFile, deleteFile, closeAllEditors, pathEquals } from './utils'; +import { createRandomFile, deleteFile, closeAllEditors, pathEquals } from '../utils'; import { join, basename } from 'path'; import * as fs from 'fs'; -import { Uri } from 'vscode'; suite('workspace-namespace', () => { @@ -39,11 +38,27 @@ suite('workspace-namespace', () => { test('rootPath', () => { if (vscode.workspace.rootPath) { - assert.ok(pathEquals(vscode.workspace.rootPath, join(__dirname, '../testWorkspace'))); + assert.ok(pathEquals(vscode.workspace.rootPath, join(__dirname, '../../testWorkspace'))); } assert.throws(() => vscode.workspace.rootPath = 'farboo'); }); + test('workspaceFolders', () => { + if (vscode.workspace.workspaceFolders) { + assert.equal(vscode.workspace.workspaceFolders.length, 1); + assert.ok(pathEquals(vscode.workspace.workspaceFolders[0].uri.fsPath, join(__dirname, '../../testWorkspace'))); + } + }); + + test('getWorkspaceFolder', () => { + const folder = vscode.workspace.getWorkspaceFolder(vscode.Uri.file(join(__dirname, '../../testWorkspace/far.js'))); + assert.ok(!!folder); + + if (folder) { + assert.ok(pathEquals(folder.uri.fsPath, join(__dirname, '../../testWorkspace'))); + } + }); + test('openTextDocument', () => { let len = vscode.workspace.textDocuments.length; return vscode.workspace.openTextDocument(join(vscode.workspace.rootPath || '', './simple.txt')).then(doc => { @@ -536,7 +551,7 @@ suite('workspace-namespace', () => { test('applyEdit should fail when editing renamed from resource', async () => { const resource = await createRandomFile(); - const newResource = Uri.parse(resource.fsPath + '.1'); + const newResource = vscode.Uri.parse(resource.fsPath + '.1'); const edit = new vscode.WorkspaceEdit(); edit.renameResource(resource, newResource); try { diff --git a/extensions/vscode-api-tests/src/workspace-tests/index.ts b/extensions/vscode-api-tests/src/workspace-tests/index.ts new file mode 100644 index 0000000000000..f65a756a8dea0 --- /dev/null +++ b/extensions/vscode-api-tests/src/workspace-tests/index.ts @@ -0,0 +1,28 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// +// PLEASE DO NOT MODIFY / DELETE UNLESS YOU KNOW WHAT YOU ARE DOING +// +// This file is providing the test runner to use when running extension tests. +// By default the test runner in use is Mocha based. +// +// You can provide your own test runner if you want to override it by exporting +// a function run(testRoot: string, clb: (error:Error) => void) that the extension +// host can call to run the tests. The test runner is expected to use console.log +// to report the results back to the caller. When the tests are finished, return +// a possible error to the callback or null if none. + +const testRunner = require('vscode/lib/testrunner'); + +// You can directly control Mocha options by uncommenting the following lines +// See https://github.com/mochajs/mocha/wiki/Using-mocha-programmatically#set-options for more info +testRunner.configure({ + ui: 'tdd', // the TDD UI is being used in extension.test.ts (suite, test, etc.) + useColors: process.platform !== 'win32', // colored output from test results (only windows cannot handle) + timeout: 60000 +}); + +export = testRunner; diff --git a/extensions/vscode-api-tests/src/workspace-tests/workspace.test.ts b/extensions/vscode-api-tests/src/workspace-tests/workspace.test.ts new file mode 100644 index 0000000000000..5453c2adec332 --- /dev/null +++ b/extensions/vscode-api-tests/src/workspace-tests/workspace.test.ts @@ -0,0 +1,40 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import * as assert from 'assert'; +import * as vscode from 'vscode'; +import { closeAllEditors, pathEquals } from '../utils'; +import { join } from 'path'; + +suite('workspace-namespace', () => { + + teardown(closeAllEditors); + + test('rootPath', () => { + if (vscode.workspace.rootPath) { + assert.ok(pathEquals(vscode.workspace.rootPath, join(__dirname, '../../testWorkspace'))); + } + }); + + test('workspaceFolders', () => { + if (vscode.workspace.workspaceFolders) { + assert.equal(vscode.workspace.workspaceFolders.length, 2); + assert.ok(pathEquals(vscode.workspace.workspaceFolders[0].uri.fsPath, join(__dirname, '../../testWorkspace'))); + assert.ok(pathEquals(vscode.workspace.workspaceFolders[1].uri.fsPath, join(__dirname, '../../testWorkspace2'))); + assert.ok(pathEquals(vscode.workspace.workspaceFolders[1].name, 'Test Workspace 2')); + } + }); + + test('getWorkspaceFolder', () => { + const folder = vscode.workspace.getWorkspaceFolder(vscode.Uri.file(join(__dirname, '../../testWorkspace2/far.js'))); + assert.ok(!!folder); + + if (folder) { + assert.ok(pathEquals(folder.uri.fsPath, join(__dirname, '../../testWorkspace2'))); + } + }); +}); diff --git a/extensions/vscode-api-tests/testWorkspace2/simple.txt b/extensions/vscode-api-tests/testWorkspace2/simple.txt new file mode 100644 index 0000000000000..b546282996658 --- /dev/null +++ b/extensions/vscode-api-tests/testWorkspace2/simple.txt @@ -0,0 +1 @@ +Just a simple file... \ No newline at end of file diff --git a/extensions/vscode-api-tests/testworkspace.code-workspace b/extensions/vscode-api-tests/testworkspace.code-workspace new file mode 100644 index 0000000000000..7e93fe23c4244 --- /dev/null +++ b/extensions/vscode-api-tests/testworkspace.code-workspace @@ -0,0 +1,11 @@ +{ + "folders": [ + { + "path": "testWorkspace" + }, + { + "path": "testWorkspace2", + "name": "Test Workspace 2" + } + ] +} \ No newline at end of file diff --git a/package.json b/package.json index f771a202e86da..0b397e34cb69e 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "node-pty": "0.7.4", "nsfw": "1.0.16", "semver": "4.3.6", - "spdlog": "0.5.0", + "spdlog": "0.6.0", "sudo-prompt": "^8.0.0", "v8-inspect-profiler": "^0.0.7", "vscode-chokidar": "1.6.2", diff --git a/scripts/test-integration.bat b/scripts/test-integration.bat index b9a95fa62c9fc..9a779446e2c24 100644 --- a/scripts/test-integration.bat +++ b/scripts/test-integration.bat @@ -9,7 +9,10 @@ if not "%APPVEYOR%" == "" ( set VSCODEUSERDATADIR=%TMP%\vscodeuserfolder-%RANDOM%-%TIME:~6,5% :: Tests in the extension host -call .\scripts\code.bat %~dp0\..\extensions\vscode-api-tests\testWorkspace --extensionDevelopmentPath=%~dp0\..\extensions\vscode-api-tests --extensionTestsPath=%~dp0\..\extensions\vscode-api-tests\out --disableExtensions --user-data-dir=%VSCODEUSERDATADIR% +call .\scripts\code.bat %~dp0\..\extensions\vscode-api-tests\testWorkspace --extensionDevelopmentPath=%~dp0\..\extensions\vscode-api-tests --extensionTestsPath=%~dp0\..\extensions\vscode-api-tests\out\singlefolder-tests --disableExtensions --user-data-dir=%VSCODEUSERDATADIR% +if %errorlevel% neq 0 exit /b %errorlevel% + +call .\scripts\code.bat %~dp0\..\extensions\vscode-api-tests\testWorkspace.code-workspace --extensionDevelopmentPath=%~dp0\..\extensions\vscode-api-tests --extensionTestsPath=%~dp0\..\extensions\vscode-api-tests\out\workspace-tests --disableExtensions --user-data-dir=%VSCODEUSERDATADIR% if %errorlevel% neq 0 exit /b %errorlevel% call .\scripts\code.bat %~dp0\..\extensions\vscode-colorize-tests\test --extensionDevelopmentPath=%~dp0\..\extensions\vscode-colorize-tests --extensionTestsPath=%~dp0\..\extensions\vscode-colorize-tests\out --disableExtensions --user-data-dir=%VSCODEUSERDATADIR% diff --git a/scripts/test-integration.sh b/scripts/test-integration.sh index 6159dc88aa98a..ba10a8cfd69f6 100755 --- a/scripts/test-integration.sh +++ b/scripts/test-integration.sh @@ -13,7 +13,8 @@ fi cd $ROOT # Tests in the extension host -./scripts/code.sh $ROOT/extensions/vscode-api-tests/testWorkspace --extensionDevelopmentPath=$ROOT/extensions/vscode-api-tests --extensionTestsPath=$ROOT/extensions/vscode-api-tests/out --disableExtensions --user-data-dir=$VSCODEUSERDATADIR --skip-getting-started +./scripts/code.sh $ROOT/extensions/vscode-api-tests/testWorkspace --extensionDevelopmentPath=$ROOT/extensions/vscode-api-tests --extensionTestsPath=$ROOT/extensions/vscode-api-tests/out/singlefolder-tests --disableExtensions --user-data-dir=$VSCODEUSERDATADIR --skip-getting-started +./scripts/code.sh $ROOT/extensions/vscode-api-tests/testWorkspace.code-workspace --extensionDevelopmentPath=$ROOT/extensions/vscode-api-tests --extensionTestsPath=$ROOT/extensions/vscode-api-tests/out/workspace-tests --disableExtensions --user-data-dir=$VSCODEUSERDATADIR --skip-getting-started ./scripts/code.sh $ROOT/extensions/vscode-colorize-tests/test --extensionDevelopmentPath=$ROOT/extensions/vscode-colorize-tests --extensionTestsPath=$ROOT/extensions/vscode-colorize-tests/out --disableExtensions --user-data-dir=$VSCODEUSERDATADIR --skip-getting-started ./scripts/code.sh $ROOT/extensions/emmet/test-fixtures --extensionDevelopmentPath=$ROOT/extensions/emmet --extensionTestsPath=$ROOT/extensions/emmet/out/test --disableExtensions --user-data-dir=$VSCODEUSERDATADIR --skip-getting-started diff --git a/src/vs/editor/common/config/commonEditorConfig.ts b/src/vs/editor/common/config/commonEditorConfig.ts index 6f6f9040b52db..12d923aa69605 100644 --- a/src/vs/editor/common/config/commonEditorConfig.ts +++ b/src/vs/editor/common/config/commonEditorConfig.ts @@ -468,6 +468,17 @@ const editorConfiguration: IConfigurationNode = { 'default': EDITOR_DEFAULTS.contribInfo.wordBasedSuggestions, 'description': nls.localize('wordBasedSuggestions', "Controls whether completions should be computed based on words in the document.") }, + 'editor.selectSuggestions': { + 'type': 'string', + 'enum': ['never', 'byRecency', 'byPrefix'], + 'enumDescriptions': [ + nls.localize('selectSuggestions.never', "Do not remember suggestions and always select the first."), + nls.localize('selectSuggestions.byRecency', "Select recent suggestions unless further typing selects one, e.g. `console.| -> console.log`"), + nls.localize('selectSuggestions.byPrefix', "Select suggestions based on previous prefixes that have completed those suggestions, e.g. `co -> console` and `con -> const`"), + ], + 'default': 'byRecency', + 'description': nls.localize('selectSuggestions', "Controls if accepting suggestions changes how future suggestions are pre-selected.") + }, 'editor.suggestFontSize': { 'type': 'integer', 'default': 0, diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index 442d8e9cff782..9b76f7ea2dceb 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -457,6 +457,10 @@ export interface IEditorOptions { * Enable word based suggestions. Defaults to 'true' */ wordBasedSuggestions?: boolean; + /** + * The history mode for suggestions. + */ + selectSuggestions?: string; /** * The font size for the suggest widget. * Defaults to the editor font size. @@ -827,6 +831,7 @@ export interface EditorContribOptions { readonly acceptSuggestionOnCommitCharacter: boolean; readonly snippetSuggestions: 'top' | 'bottom' | 'inline' | 'none'; readonly wordBasedSuggestions: boolean; + readonly selectSuggestions: 'never' | 'byRecency' | 'byPrefix'; readonly suggestFontSize: number; readonly suggestLineHeight: number; readonly selectionHighlight: boolean; @@ -1176,6 +1181,7 @@ export class InternalEditorOptions { && a.acceptSuggestionOnCommitCharacter === b.acceptSuggestionOnCommitCharacter && a.snippetSuggestions === b.snippetSuggestions && a.wordBasedSuggestions === b.wordBasedSuggestions + && a.selectSuggestions === b.selectSuggestions && a.suggestFontSize === b.suggestFontSize && a.suggestLineHeight === b.suggestLineHeight && a.selectionHighlight === b.selectionHighlight @@ -1706,6 +1712,7 @@ export class EditorOptionsValidator { acceptSuggestionOnCommitCharacter: _boolean(opts.acceptSuggestionOnCommitCharacter, defaults.acceptSuggestionOnCommitCharacter), snippetSuggestions: _stringSet<'top' | 'bottom' | 'inline' | 'none'>(opts.snippetSuggestions, defaults.snippetSuggestions, ['top', 'bottom', 'inline', 'none']), wordBasedSuggestions: _boolean(opts.wordBasedSuggestions, defaults.wordBasedSuggestions), + selectSuggestions: _stringSet<'never' | 'byRecency' | 'byPrefix'>(opts.selectSuggestions, defaults.selectSuggestions, ['never', 'byRecency', 'byPrefix']), suggestFontSize: _clampedInt(opts.suggestFontSize, defaults.suggestFontSize, 0, 1000), suggestLineHeight: _clampedInt(opts.suggestLineHeight, defaults.suggestLineHeight, 0, 1000), selectionHighlight: _boolean(opts.selectionHighlight, defaults.selectionHighlight), @@ -1806,6 +1813,7 @@ export class InternalEditorOptionsFactory { acceptSuggestionOnCommitCharacter: opts.contribInfo.acceptSuggestionOnCommitCharacter, snippetSuggestions: opts.contribInfo.snippetSuggestions, wordBasedSuggestions: opts.contribInfo.wordBasedSuggestions, + selectSuggestions: opts.contribInfo.selectSuggestions, suggestFontSize: opts.contribInfo.suggestFontSize, suggestLineHeight: opts.contribInfo.suggestLineHeight, selectionHighlight: (accessibilityIsOn ? false : opts.contribInfo.selectionHighlight), // DISABLED WHEN SCREEN READER IS ATTACHED @@ -2260,6 +2268,7 @@ export const EDITOR_DEFAULTS: IValidatedEditorOptions = { acceptSuggestionOnCommitCharacter: true, snippetSuggestions: 'inline', wordBasedSuggestions: true, + selectSuggestions: 'byRecency', suggestFontSize: 0, suggestLineHeight: 0, selectionHighlight: true, diff --git a/src/vs/editor/common/modes.ts b/src/vs/editor/common/modes.ts index a09587997eebd..c7fd1b7b243f2 100644 --- a/src/vs/editor/common/modes.ts +++ b/src/vs/editor/common/modes.ts @@ -856,8 +856,14 @@ export interface WorkspaceEdit { rejectReason?: string; // TODO@joh, move to rename } +export interface RenameInitialValue { + range: IRange; + text?: string; +} + export interface RenameProvider { provideRenameEdits(model: model.ITextModel, position: Position, newName: string, token: CancellationToken): WorkspaceEdit | Thenable; + resolveInitialRenameValue?(model: model.ITextModel, position: Position, token: CancellationToken): RenameInitialValue | Thenable; } diff --git a/src/vs/editor/contrib/rename/rename.ts b/src/vs/editor/contrib/rename/rename.ts index b25b9b3f74b0c..e164c10e766e1 100644 --- a/src/vs/editor/contrib/rename/rename.ts +++ b/src/vs/editor/contrib/rename/rename.ts @@ -6,7 +6,7 @@ 'use strict'; import * as nls from 'vs/nls'; -import { isPromiseCanceledError, illegalArgument } from 'vs/base/common/errors'; +import { isPromiseCanceledError, illegalArgument, onUnexpectedExternalError } from 'vs/base/common/errors'; import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; import Severity from 'vs/base/common/severity'; import { TPromise } from 'vs/base/common/winjs.base'; @@ -25,7 +25,7 @@ import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { optional } from 'vs/platform/instantiation/common/instantiation'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { sequence, asWinJsPromise } from 'vs/base/common/async'; -import { WorkspaceEdit, RenameProviderRegistry } from 'vs/editor/common/modes'; +import { WorkspaceEdit, RenameProviderRegistry, RenameInitialValue } from 'vs/editor/common/modes'; import { Position } from 'vs/editor/common/core/position'; import { alert } from 'vs/base/browser/ui/aria/aria'; import { Range } from 'vs/editor/common/core/range'; @@ -79,6 +79,19 @@ export function rename(model: ITextModel, position: Position, newName: string): }); } +function resolveInitialRenameValue(model: ITextModel, position: Position): TPromise { + const supports = RenameProviderRegistry.ordered(model); + return asWinJsPromise((token) => + supports.length > 0 + ? supports[0].resolveInitialRenameValue(model, position, token) //Use first rename provider so that we always use the same for resolving the location and for the actual rename + : undefined + ).then(result => { + return !result ? undefined : result; + }, err => { + onUnexpectedExternalError(err); + return TPromise.wrapError(new Error('provider failed')); + }); +} // --- register actions and commands @@ -116,34 +129,64 @@ class RenameController implements IEditorContribution { return RenameController.ID; } - public run(): TPromise { + public async run(): TPromise { + const selection = this.editor.getSelection(); + + let lineNumber = selection.startLineNumber, + selectionStart = 0, + selectionEnd = 0, + wordRange: Range, + word: string; - const selection = this.editor.getSelection(), - word = this.editor.getModel().getWordAtPosition(selection.getStartPosition()); + let initialValue = await resolveInitialRenameValue(this.editor.getModel(), this.editor.getPosition()); + + if(initialValue) { + lineNumber = initialValue.range.startLineNumber; + if(initialValue.text) { + word = initialValue.text; + } + else { + word = this.editor.getModel().getValueInRange(initialValue.range); + } + selectionEnd = word.length; + + if (!selection.isEmpty() && selection.startLineNumber === selection.endLineNumber) { + selectionStart = Math.max(0, selection.startColumn - initialValue.range.startColumn); + selectionEnd = Math.min(initialValue.range.endColumn, selection.endColumn) - initialValue.range.startColumn; + } + + wordRange = new Range( + lineNumber, + initialValue.range.startColumn, + lineNumber, + initialValue.range.endColumn + ); - if (!word) { - return undefined; } + else { + const wordAtPosition = this.editor.getModel().getWordAtPosition(selection.getStartPosition()); - let lineNumber = selection.startLineNumber, - selectionStart = 0, - selectionEnd = word.word.length, - wordRange: Range; - - wordRange = new Range( - lineNumber, - word.startColumn, - lineNumber, - word.endColumn - ); - - if (!selection.isEmpty() && selection.startLineNumber === selection.endLineNumber) { - selectionStart = Math.max(0, selection.startColumn - word.startColumn); - selectionEnd = Math.min(word.endColumn, selection.endColumn) - word.startColumn; + if (!wordAtPosition) { + return undefined; + } + word = wordAtPosition.word; + selectionEnd = word.length; + + if (!selection.isEmpty() && selection.startLineNumber === selection.endLineNumber) { + selectionStart = Math.max(0, selection.startColumn - wordAtPosition.startColumn); + selectionEnd = Math.min(wordAtPosition.endColumn, selection.endColumn) - wordAtPosition.startColumn; + } + + wordRange = new Range( + lineNumber, + wordAtPosition.startColumn, + lineNumber, + wordAtPosition.endColumn + ); } this._renameInputVisible.set(true); - return this._renameInputField.getInput(wordRange, word.word, selectionStart, selectionEnd).then(newName => { + return this._renameInputField.getInput(wordRange, word, selectionStart, selectionEnd).then(newName => { this._renameInputVisible.reset(); this.editor.focus(); @@ -166,7 +209,7 @@ class RenameController implements IEditorContribution { this.editor.setSelection(selection); } // alert - alert(nls.localize('aria', "Successfully renamed '{0}' to '{1}'. Summary: {2}", word.word, newName, edit.ariaMessage())); + alert(nls.localize('aria', "Successfully renamed '{0}' to '{1}'. Summary: {2}", word, newName, edit.ariaMessage())); }); }, err => { diff --git a/src/vs/editor/contrib/suggest/suggestController.ts b/src/vs/editor/contrib/suggest/suggestController.ts index e3eed8c43107b..b3a73b17cd995 100644 --- a/src/vs/editor/contrib/suggest/suggestController.ts +++ b/src/vs/editor/contrib/suggest/suggestController.ts @@ -95,7 +95,7 @@ export class SuggestController implements IEditorContribution { @IInstantiationService private _instantiationService: IInstantiationService, ) { this._model = new SuggestModel(this._editor); - this._memory = _instantiationService.createInstance(SuggestMemories); + this._memory = _instantiationService.createInstance(SuggestMemories, this._editor.getConfiguration().contribInfo.selectSuggestions); this._toDispose.push(this._model.onDidTrigger(e => { if (!this._widget) { @@ -103,29 +103,22 @@ export class SuggestController implements IEditorContribution { } this._widget.showTriggered(e.auto); })); - let lastSelectedItem: ICompletionItem; this._toDispose.push(this._model.onDidSuggest(e => { - let index = this._memory.select(this._editor.getModel().getLanguageIdentifier(), e.completionModel.items, lastSelectedItem); - if (index >= 0) { - lastSelectedItem = e.completionModel.items[index]; - } else { - index = 0; - lastSelectedItem = undefined; - } + let index = this._memory.select(this._editor.getModel(), this._editor.getPosition(), e.completionModel.items); this._widget.showSuggestions(e.completionModel, index, e.isFrozen, e.auto); })); this._toDispose.push(this._model.onDidCancel(e => { if (this._widget && !e.retrigger) { this._widget.hideWidget(); - lastSelectedItem = undefined; } })); // Manage the acceptSuggestionsOnEnter context key let acceptSuggestionsOnEnter = SuggestContext.AcceptSuggestionsOnEnter.bindTo(_contextKeyService); let updateFromConfig = () => { - const { acceptSuggestionOnEnter } = this._editor.getConfiguration().contribInfo; + const { acceptSuggestionOnEnter, selectSuggestions } = this._editor.getConfiguration().contribInfo; acceptSuggestionsOnEnter.set(acceptSuggestionOnEnter === 'on' || acceptSuggestionOnEnter === 'smart'); + this._memory.setMode(selectSuggestions); }; this._toDispose.push(this._editor.onDidChangeConfiguration((e) => updateFromConfig())); updateFromConfig(); @@ -209,12 +202,8 @@ export class SuggestController implements IEditorContribution { this._editor.pushUndoStop(); } - // remember this suggestion for future invocations - // when it wasn't the first suggestion but from the group - // of top suggestions (cons -> const, console, constructor) - if (event.model.items[0].score === event.item.score) { - this._memory.remember(this._editor.getModel().getLanguageIdentifier(), event.item); - } + // keep item in memory + this._memory.memorize(this._editor.getModel(), this._editor.getPosition(), event.item); let { insertText } = suggestion; if (suggestion.snippetType !== 'textmate') { diff --git a/src/vs/editor/contrib/suggest/suggestMemory.ts b/src/vs/editor/contrib/suggest/suggestMemory.ts index 46a02a46e8546..e434231cf0e4b 100644 --- a/src/vs/editor/contrib/suggest/suggestMemory.ts +++ b/src/vs/editor/contrib/suggest/suggestMemory.ts @@ -5,119 +5,210 @@ 'use strict'; import { ICompletionItem } from 'vs/editor/contrib/suggest/completionModel'; -import { LRUCache } from 'vs/base/common/map'; -import { LanguageIdentifier } from 'vs/editor/common/modes'; +import { LRUCache, TernarySearchTree } from 'vs/base/common/map'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; +import { ITextModel } from 'vs/editor/common/model'; +import { IPosition } from 'vs/editor/common/core/position'; +import { RunOnceScheduler } from 'vs/base/common/async'; -export class SuggestMemories { +export abstract class Memory { - private readonly _storagePrefix = 'suggest/memories'; - private readonly _data = new Map(); + abstract memorize(model: ITextModel, pos: IPosition, item: ICompletionItem): void; - constructor( - @IStorageService private _storageService: IStorageService - ) { + abstract select(model: ITextModel, pos: IPosition, items: ICompletionItem[]): number; + + abstract toJSON(): object; + + abstract fromJSON(data: object): void; +} + +export class NoMemory extends Memory { + + memorize(model: ITextModel, pos: IPosition, item: ICompletionItem): void { + // no-op + } + + select(model: ITextModel, pos: IPosition, items: ICompletionItem[]): number { + return 0; + } + + toJSON() { + return undefined; + } + + fromJSON() { // } +} - remember({ language }: LanguageIdentifier, item: ICompletionItem): void { - let memory = this._data.get(language); - if (!memory) { - memory = new SuggestMemory(); - this._data.set(language, memory); - } - memory.remember(item); - this._storageService.store(`${this._storagePrefix}/${language}`, JSON.stringify(memory), StorageScope.WORKSPACE); - } - - select({ language }: LanguageIdentifier, items: ICompletionItem[], last: ICompletionItem): number { - let memory = this._data.get(language); - if (!memory) { - const key: string = `${this._storagePrefix}/${language}`; - const raw = this._storageService.get(key, StorageScope.WORKSPACE); - if (raw) { - try { - const tuples = <[string, MemoryItem][]>JSON.parse(raw); - memory = new SuggestMemory(tuples); - last = undefined; - this._data.set(language, memory); - } catch (e) { - this._storageService.remove(key, StorageScope.WORKSPACE); +export interface MemItem { + type: string; + insertText: string; + touch: number; +} + +export class LRUMemory extends Memory { + + private _cache = new LRUCache(300, .66); + private _seq = 0; + + memorize(model: ITextModel, pos: IPosition, item: ICompletionItem): void { + const { label } = item.suggestion; + const key = `${model.getLanguageIdentifier().language}/${label}`; + this._cache.set(key, { + touch: this._seq++, + type: item.suggestion.type, + insertText: undefined + }); + } + + select(model: ITextModel, pos: IPosition, items: ICompletionItem[]): number { + // in order of completions, select the first + // that has been used in the past + let { word } = model.getWordUntilPosition(pos); + + let res = 0; + let seq = -1; + if (word.length === 0) { + for (let i = 0; i < items.length; i++) { + const { suggestion } = items[i]; + const key = `${model.getLanguageIdentifier().language}/${suggestion.label}`; + const item = this._cache.get(key); + if (item && item.touch > seq && item.type === suggestion.type) { + seq = item.touch; + res = i; } } } - if (memory) { - return memory.select(items, last); - } else { - return -1; + return res; + } + + toJSON(): object { + let data: [string, MemItem][] = []; + this._cache.forEach((value, key) => { + data.push([key, value]); + }); + return data; + } + + fromJSON(data: [string, MemItem][]): void { + this._cache.clear(); + let seq = 0; + for (const [key, value] of data) { + value.touch = seq; + this._cache.set(key, value); } + this._seq = this._cache.size; } } -export interface MemoryItem { - type: string; - insertText: string; -} +export class PrefixMemory extends Memory { -export class SuggestMemory { + private _trie = TernarySearchTree.forStrings(); + private _seq = 0; - private readonly _memory = new LRUCache(400, 0.75); + memorize(model: ITextModel, pos: IPosition, item: ICompletionItem): void { + const { word } = model.getWordUntilPosition(pos); + const key = `${model.getLanguageIdentifier().language}/${word}`; + this._trie.set(key, { + type: item.suggestion.type, + insertText: item.suggestion.insertText, + touch: this._seq++ + }); + } - constructor(tuples?: [string, MemoryItem][]) { - if (tuples) { - for (const [word, item] of tuples) { - this._memory.set(word, item); + select(model: ITextModel, pos: IPosition, items: ICompletionItem[]): number { + let { word } = model.getWordUntilPosition(pos); + if (!word) { + return 0; + } + let key = `${model.getLanguageIdentifier().language}/${word}`; + let item = this._trie.get(key); + if (!item) { + item = this._trie.findSubstr(key); + } + if (item) { + for (let i = 0; i < items.length; i++) { + let { type, insertText } = items[i].suggestion; + if (type === item.type && insertText === item.insertText) { + return i; + } } } + return 0; } - remember(item: ICompletionItem): void { - if (item.word) { - this._memory.set(item.word, { insertText: item.suggestion.insertText, type: item.suggestion.type }); + toJSON(): object { + + let entries: [string, MemItem][] = []; + this._trie.forEach((value, key) => entries.push([key, value])); + + // sort by last recently used (touch), then + // take the top 200 item and normalize their + // touch + entries + .sort((a, b) => -(a[1].touch - b[1].touch)) + .forEach((value, i) => value[1].touch = i); + + return entries.slice(0, 200); + } + + fromJSON(data: [string, MemItem][]): void { + this._trie.clear(); + if (data.length > 0) { + this._seq = data[0][1].touch + 1; + for (const [key, value] of data) { + this._trie.set(key, value); + } } } +} - select(items: ICompletionItem[], last: ICompletionItem): number { +export type MemMode = 'never' | 'byRecency' | 'byPrefix'; - if (items.length === 0) { - return -1; - } +export class SuggestMemories { - const topScore = items[0].score; + private readonly _storagePrefix = 'suggest/memories'; - for (let i = 0; i < items.length; i++) { + private _mode: MemMode; + private _strategy: Memory; + private _persistSoon: RunOnceScheduler; - if (topScore !== items[i].score) { - // we only take a look at the bucket - // of top matches, hence we return - // as soon as we see an item that - // hasn't the top score anymore - return -1; - } + constructor( + mode: MemMode, + @IStorageService private _storageService: IStorageService + ) { + this._persistSoon = new RunOnceScheduler(() => this._flush(), 3000); + this.setMode(mode); + } - if (items[i] === last) { - // prefer the last selected item when - // there is one - return i; - } - if (items[i].word) { - const item = this._memory.get(items[i].word); - if (this._matches(item, items[i])) { - return i; - } - } + setMode(mode: MemMode): void { + if (this._mode === mode) { + return; } - return -1; + this._mode = mode; + this._strategy = mode === 'byPrefix' ? new PrefixMemory() : mode === 'byRecency' ? new LRUMemory() : new NoMemory(); + + try { + const raw = this._storageService.get(`${this._storagePrefix}/${this._mode}`, StorageScope.WORKSPACE); + this._strategy.fromJSON(JSON.parse(raw)); + } catch (e) { + // things can go wrong with JSON... + } + } + + memorize(model: ITextModel, pos: IPosition, item: ICompletionItem): void { + this._strategy.memorize(model, pos, item); + this._persistSoon.schedule(); } - private _matches(item: MemoryItem, candidate: ICompletionItem): boolean { - return item && item.insertText === candidate.suggestion.insertText && item.type === candidate.suggestion.type; + select(model: ITextModel, pos: IPosition, items: ICompletionItem[]): number { + return this._strategy.select(model, pos, items); } - toJSON(): [string, MemoryItem][] { - const tuples: [string, MemoryItem][] = []; - this._memory.forEach((value, key) => tuples.push([key, value])); - return tuples; + private _flush() { + const raw = JSON.stringify(this._strategy); + this._storageService.store(`${this._storagePrefix}/${this._mode}`, raw, StorageScope.WORKSPACE); } } diff --git a/src/vs/editor/contrib/suggest/test/completionModel.test.ts b/src/vs/editor/contrib/suggest/test/completionModel.test.ts index 7d78bfe145195..e86f045c606a7 100644 --- a/src/vs/editor/contrib/suggest/test/completionModel.test.ts +++ b/src/vs/editor/contrib/suggest/test/completionModel.test.ts @@ -11,37 +11,37 @@ import { CompletionModel } from 'vs/editor/contrib/suggest/completionModel'; import { IPosition } from 'vs/editor/common/core/position'; import { TPromise } from 'vs/base/common/winjs.base'; -suite('CompletionModel', function () { - - function createSuggestItem(label: string, overwriteBefore: number, type: SuggestionType = 'property', incomplete: boolean = false, position: IPosition = { lineNumber: 1, column: 1 }): ISuggestionItem { +export function createSuggestItem(label: string, overwriteBefore: number, type: SuggestionType = 'property', incomplete: boolean = false, position: IPosition = { lineNumber: 1, column: 1 }): ISuggestionItem { - return new class implements ISuggestionItem { + return new class implements ISuggestionItem { - position = position; + position = position; - suggestion: ISuggestion = { - label, - overwriteBefore, - insertText: label, - type - }; - - container: ISuggestResult = { - incomplete, - suggestions: [this.suggestion] - }; + suggestion: ISuggestion = { + label, + overwriteBefore, + insertText: label, + type + }; - support: ISuggestSupport = { - provideCompletionItems(): any { - return; - } - }; + container: ISuggestResult = { + incomplete, + suggestions: [this.suggestion] + }; - resolve(): TPromise { - return null; + support: ISuggestSupport = { + provideCompletionItems(): any { + return; } }; - } + + resolve(): TPromise { + return null; + } + }; +} +suite('CompletionModel', function () { + let model: CompletionModel; diff --git a/src/vs/editor/contrib/suggest/test/suggestMemory.test.ts b/src/vs/editor/contrib/suggest/test/suggestMemory.test.ts new file mode 100644 index 0000000000000..9d69016057f69 --- /dev/null +++ b/src/vs/editor/contrib/suggest/test/suggestMemory.test.ts @@ -0,0 +1,86 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { LRUMemory, NoMemory, PrefixMemory } from 'vs/editor/contrib/suggest/suggestMemory'; +import { ITextModel } from 'vs/editor/common/model'; +import { TextModel } from 'vs/editor/common/model/textModel'; +import { ICompletionItem } from 'vs/editor/contrib/suggest/completionModel'; +import { createSuggestItem } from 'vs/editor/contrib/suggest/test/completionModel.test'; +import { IPosition } from 'vs/editor/common/core/position'; + +suite('SuggestMemories', function () { + + let pos: IPosition; + let buffer: ITextModel; + let items: ICompletionItem[]; + + setup(function () { + pos = { lineNumber: 1, column: 1 }; + buffer = TextModel.createFromString('This is some text'); + items = [ + createSuggestItem('foo', 0), + createSuggestItem('bar', 0) + ]; + }); + + test('NoMemory', function () { + + const mem = new NoMemory(); + + assert.equal(mem.select(buffer, pos, items), 0); + assert.equal(mem.select(buffer, pos, []), 0); + + mem.memorize(buffer, pos, items[0]); + mem.memorize(buffer, pos, null); + }); + + test('ShyMemories', function () { + + const mem = new LRUMemory(); + mem.memorize(buffer, pos, items[1]); + + assert.equal(mem.select(buffer, pos, items), 1); + assert.equal(mem.select(buffer, { lineNumber: 1, column: 3 }, items), 0); + + mem.memorize(buffer, pos, items[0]); + assert.equal(mem.select(buffer, pos, items), 0); + + assert.equal(mem.select(buffer, pos, [ + createSuggestItem('new', 0), + createSuggestItem('bar', 0) + ]), 1); + + assert.equal(mem.select(buffer, pos, [ + createSuggestItem('new1', 0), + createSuggestItem('new2', 0) + ]), 0); + + }); + + test('PrefixMemory', function () { + + const mem = new PrefixMemory(); + buffer.setValue('constructor'); + const item0 = createSuggestItem('console', 0); + const item1 = createSuggestItem('const', 0); + const item2 = createSuggestItem('constructor', 0); + const item3 = createSuggestItem('constant', 0); + const items = [item0, item1, item2, item3]; + + mem.memorize(buffer, { lineNumber: 1, column: 2 }, item1); // c -> const + mem.memorize(buffer, { lineNumber: 1, column: 3 }, item0); // co -> console + mem.memorize(buffer, { lineNumber: 1, column: 4 }, item2); // con -> constructor + + assert.equal(mem.select(buffer, { lineNumber: 1, column: 1 }, items), 0); + assert.equal(mem.select(buffer, { lineNumber: 1, column: 2 }, items), 1); + assert.equal(mem.select(buffer, { lineNumber: 1, column: 3 }, items), 0); + assert.equal(mem.select(buffer, { lineNumber: 1, column: 4 }, items), 2); + assert.equal(mem.select(buffer, { lineNumber: 1, column: 7 }, items), 2); // find substr + }); + +}); diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index c4adbda4a9131..de1d4a87e4504 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -2742,6 +2742,10 @@ declare module monaco.editor { * Enable word based suggestions. Defaults to 'true' */ wordBasedSuggestions?: boolean; + /** + * The history mode for suggestions. + */ + selectSuggestions?: string; /** * The font size for the suggest widget. * Defaults to the editor font size. @@ -3048,6 +3052,7 @@ declare module monaco.editor { readonly acceptSuggestionOnCommitCharacter: boolean; readonly snippetSuggestions: 'top' | 'bottom' | 'inline' | 'none'; readonly wordBasedSuggestions: boolean; + readonly selectSuggestions: 'never' | 'byRecency' | 'byPrefix'; readonly suggestFontSize: number; readonly suggestLineHeight: number; readonly selectionHighlight: boolean; @@ -4951,8 +4956,14 @@ declare module monaco.languages { rejectReason?: string; } + export interface RenameInitialValue { + range: IRange; + text?: string; + } + export interface RenameProvider { provideRenameEdits(model: editor.ITextModel, position: Position, newName: string, token: CancellationToken): WorkspaceEdit | Thenable; + resolveInitialRenameValue?(model: editor.ITextModel, position: Position, token: CancellationToken): RenameInitialValue | Thenable; } export interface Command { diff --git a/src/vs/platform/extensionManagement/common/extensionEnablementService.ts b/src/vs/platform/extensionManagement/common/extensionEnablementService.ts index 8e4c9917668dc..1a787fc24c12a 100644 --- a/src/vs/platform/extensionManagement/common/extensionEnablementService.ts +++ b/src/vs/platform/extensionManagement/common/extensionEnablementService.ts @@ -9,7 +9,7 @@ import { distinct, coalesce } from 'vs/base/common/arrays'; import Event, { Emitter } from 'vs/base/common/event'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { IExtensionManagementService, DidUninstallExtensionEvent, IExtensionEnablementService, IExtensionIdentifier, EnablementState, ILocalExtension, isIExtensionIdentifier } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { adoptToGalleryExtensionId, getIdFromLocalExtensionId, areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; +import { adoptToGalleryExtensionId, getIdFromLocalExtensionId, areSameExtensions, getGalleryExtensionIdFromLocal } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; @@ -89,7 +89,7 @@ export class ExtensionEnablementService implements IExtensionEnablementService { if (!this.canChangeEnablement(arg)) { return TPromise.wrap(false); } - identifier = { id: getIdFromLocalExtensionId(arg.identifier.id), uuid: arg.identifier.uuid }; + identifier = { id: getGalleryExtensionIdFromLocal(arg), uuid: arg.identifier.uuid }; } const workspace = newState === EnablementState.WorkspaceDisabled || newState === EnablementState.WorkspaceEnabled; diff --git a/src/vs/platform/extensionManagement/node/extensionManagementService.ts b/src/vs/platform/extensionManagement/node/extensionManagementService.ts index 5bc3e7407512f..4688278bde0ea 100644 --- a/src/vs/platform/extensionManagement/node/extensionManagementService.ts +++ b/src/vs/platform/extensionManagement/node/extensionManagementService.ts @@ -712,7 +712,7 @@ export class ExtensionManagementService implements IExtensionManagementService { if (manifest.extensionDependencies) { manifest.extensionDependencies = manifest.extensionDependencies.map(id => adoptToGalleryExtensionId(id)); } - const identifier = { id, uuid: metadata ? metadata.id : null }; + const identifier = { id: type === LocalExtensionType.System ? id : getLocalExtensionIdFromManifest(manifest), uuid: metadata ? metadata.id : null }; return { type, identifier, manifest, metadata, path: extensionPath, readmeUrl, changelogUrl }; }); }).then(null, () => null); diff --git a/src/vs/platform/extensionManagement/test/common/extensionEnablementService.test.ts b/src/vs/platform/extensionManagement/test/common/extensionEnablementService.test.ts index c4da64ca7cf9c..63d92406632ae 100644 --- a/src/vs/platform/extensionManagement/test/common/extensionEnablementService.test.ts +++ b/src/vs/platform/extensionManagement/test/common/extensionEnablementService.test.ts @@ -331,9 +331,12 @@ suite('ExtensionEnablementService Test', () => { }); function aLocalExtension(id: string, contributes?: IExtensionContributions): ILocalExtension { + const [publisher, name] = id.split('.'); return Object.create({ identifier: { id }, manifest: { + name, + publisher, contributes } }); diff --git a/src/vs/platform/log/common/log.ts b/src/vs/platform/log/common/log.ts index 97418ed6f9abb..915e59c0df7e6 100644 --- a/src/vs/platform/log/common/log.ts +++ b/src/vs/platform/log/common/log.ts @@ -23,6 +23,8 @@ export enum LogLevel { Off } +export const DEFAULT_LOG_LEVEL: LogLevel = LogLevel.Info; + export interface ILogService extends IDisposable { _serviceBrand: any; onDidChangeLogLevel: Event; @@ -39,7 +41,7 @@ export interface ILogService extends IDisposable { export abstract class AbstractLogService extends Disposable { - private level: LogLevel = LogLevel.Error; + private level: LogLevel = DEFAULT_LOG_LEVEL; private readonly _onDidChangeLogLevel: Emitter = this._register(new Emitter()); readonly onDidChangeLogLevel: Event = this._onDidChangeLogLevel.event; @@ -60,7 +62,7 @@ export class ConsoleLogMainService extends AbstractLogService implements ILogSer _serviceBrand: any; private useColors: boolean; - constructor(logLevel: LogLevel = LogLevel.Error) { + constructor(logLevel: LogLevel = DEFAULT_LOG_LEVEL) { super(); this.setLevel(logLevel); this.useColors = !isWindows; @@ -135,7 +137,7 @@ export class ConsoleLogService extends AbstractLogService implements ILogService _serviceBrand: any; - constructor(logLevel: LogLevel = LogLevel.Error) { + constructor(logLevel: LogLevel = DEFAULT_LOG_LEVEL) { super(); this.setLevel(logLevel); } @@ -322,5 +324,5 @@ export function getLogLevel(environmentService: IEnvironmentService): LogLevel { return LogLevel.Off; } } - return LogLevel.Info; + return DEFAULT_LOG_LEVEL; } \ No newline at end of file diff --git a/src/vs/platform/log/node/spdlogService.ts b/src/vs/platform/log/node/spdlogService.ts index 8627fc959eedb..4f096a1c70c53 100644 --- a/src/vs/platform/log/node/spdlogService.ts +++ b/src/vs/platform/log/node/spdlogService.ts @@ -9,11 +9,10 @@ import * as path from 'path'; import { ILogService, LogLevel, NullLogService, AbstractLogService } from 'vs/platform/log/common/log'; import { RotatingLogger, setAsyncMode } from 'spdlog'; -export function createSpdLogService(processName: string, logLevel: LogLevel, logsFolder: string, logsSubfolder?: string): ILogService { +export function createSpdLogService(processName: string, logLevel: LogLevel, logsFolder: string): ILogService { try { setAsyncMode(8192, 2000); - const logsDirPath = logsSubfolder ? path.join(logsFolder, logsSubfolder) : logsFolder; - const logfilePath = path.join(logsDirPath, `${processName}.log`); + const logfilePath = path.join(logsFolder, `${processName}.log`); const logger = new RotatingLogger(processName, logfilePath, 1024 * 1024 * 5, 6); logger.setLevel(0); diff --git a/src/vs/platform/telemetry/common/telemetryUtils.ts b/src/vs/platform/telemetry/common/telemetryUtils.ts index 1b3b0233b7b60..73b4bcf93cb4d 100644 --- a/src/vs/platform/telemetry/common/telemetryUtils.ts +++ b/src/vs/platform/telemetry/common/telemetryUtils.ts @@ -98,6 +98,7 @@ const configurationValueWhitelist = [ 'editor.snippetSuggestions', 'editor.emptySelectionClipboard', 'editor.wordBasedSuggestions', + 'editor.selectSuggestions', 'editor.suggestFontSize', 'editor.suggestLineHeight', 'editor.selectionHighlight', diff --git a/src/vs/platform/workspace/common/workspace.ts b/src/vs/platform/workspace/common/workspace.ts index 1493935a0130b..9ba87d6253599 100644 --- a/src/vs/platform/workspace/common/workspace.ts +++ b/src/vs/platform/workspace/common/workspace.ts @@ -205,8 +205,8 @@ export class Workspace implements IWorkspace { export class WorkspaceFolder implements IWorkspaceFolder { readonly uri: URI; - readonly name: string; - readonly index: number; + name: string; + index: number; constructor(data: IWorkspaceFolderData, readonly raw?: IStoredWorkspaceFolder) { diff --git a/src/vs/vscode.d.ts b/src/vs/vscode.d.ts index 60976131c0273..b1ca5266c4f29 100644 --- a/src/vs/vscode.d.ts +++ b/src/vs/vscode.d.ts @@ -2664,6 +2664,11 @@ declare module 'vscode' { appendVariable(name: string, defaultValue: string | ((snippet: SnippetString) => any)): SnippetString; } + export interface RenameInitialValue { + range: Range + text?: string + } + /** * The rename provider interface defines the contract between extensions and * the [rename](https://code.visualstudio.com/docs/editor/editingevolved#_rename-symbol)-feature. @@ -2682,6 +2687,8 @@ declare module 'vscode' { * signaled by returning `undefined` or `null`. */ provideRenameEdits(document: TextDocument, position: Position, newName: string, token: CancellationToken): ProviderResult; + + resolveInitialRenameValue?(document: TextDocument, position: Position, token: CancellationToken): ProviderResult; } /** diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 4551f8a4f2aca..ef2c05f37c635 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -157,6 +157,46 @@ declare module 'vscode' { export namespace workspace { export function registerFileSystemProvider(scheme: string, provider: FileSystemProvider): Disposable; + + /** + * Updates the workspace folders of the currently opened workspace. This method allows to add, remove + * and change workspace folders a the same time. Use the [onDidChangeWorkspaceFolders()](#onDidChangeWorkspaceFolders) + * event to get notified when the workspace folders have been updated. + * + * **Example:** adding a new workspace folder at the end of workspace folders + * ```typescript + * workspace.updateWorkspaceFolders(workspace.workspaceFolders ? workspace.workspaceFolders.length : 0, null, { uri: ...}); + * ``` + * + * **Example:** removing the first workspace folder + * ```typescript + * workspace.updateWorkspaceFolders(0, 1); + * ``` + * + * **Example:** replacing an existing workspace folder with a new one + * ```typescript + * workspace.updateWorkspaceFolders(0, 1, { uri: ...}); + * ``` + * + * It is valid to remove an existing workspace folder and add it again with a different name + * to rename that folder. + * + * Note: if the first workspace folder is added, removed or changed, all extensions will be restarted + * so that the (deprecated) `rootPath` property is updated to point to the first workspace + * folder. + * + * Note: it is not valid to call [updateWorkspaceFolders()](#updateWorkspaceFolders) multiple times + * without waiting for the [onDidChangeWorkspaceFolders()](#onDidChangeWorkspaceFolders) to fire. + * + * @param start the zero-based location in the list of currently opened [workspace folders](#WorkspaceFolder) + * from which to start deleting workspace folders. + * @param deleteCount the optional number of workspace folders to remove. + * @param workspaceFoldersToAdd the optional variable set of workspace folders to add in place of the deleted ones. + * Each workspace is identified with a mandatory URI and an optional name. + * @return true if the operation was successfully started and false otherwise if arguments were used that would result + * in invalid workspace folder state (e.g. 2 folders with the same URI). + */ + export function updateWorkspaceFolders(start: number, deleteCount: number, ...workspaceFoldersToAdd: { uri: Uri, name?: string }[]): boolean; } export namespace window { @@ -240,13 +280,13 @@ declare module 'vscode' { * Add breakpoints. * @param breakpoints The breakpoints to add. */ - export function addBreakpoints(breakpoints: Breakpoint[]): Thenable; + export function addBreakpoints(breakpoints: Breakpoint[]): void; /** * Remove breakpoints. * @param breakpoints The breakpoints to remove. */ - export function removeBreakpoints(breakpoints: Breakpoint[]): Thenable; + export function removeBreakpoints(breakpoints: Breakpoint[]): void; } /** diff --git a/src/vs/workbench/api/electron-browser/mainThreadDebugService.ts b/src/vs/workbench/api/electron-browser/mainThreadDebugService.ts index 82c4bf0793796..46140b8f54b12 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadDebugService.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadDebugService.ts @@ -11,7 +11,7 @@ import { TPromise } from 'vs/base/common/winjs.base'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { ExtHostContext, ExtHostDebugServiceShape, MainThreadDebugServiceShape, DebugSessionUUID, MainContext, - IExtHostContext, IBreakpointsDeltaDto, ISourceMultiBreakpointDto, ISourceBreakpointDto, IFunctionBreakpointDto, IBreakpointIndexDto + IExtHostContext, IBreakpointsDeltaDto, ISourceMultiBreakpointDto, ISourceBreakpointDto, IFunctionBreakpointDto } from '../node/extHost.protocol'; import { extHostNamedCustomer } from 'vs/workbench/api/electron-browser/extHostCustomers'; import severity from 'vs/base/common/severity'; @@ -92,14 +92,13 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape { return TPromise.wrap(undefined); } - public async $registerBreakpoints(DTOs: (ISourceMultiBreakpointDto | IFunctionBreakpointDto)[]): TPromise { + public $registerBreakpoints(DTOs: (ISourceMultiBreakpointDto | IFunctionBreakpointDto)[]): TPromise { - const result: IBreakpointIndexDto[] = []; for (let dto of DTOs) { if (dto.type === 'sourceMulti') { - const sdto = dto; const rawbps = dto.lines.map(l => { + id: l.id, enabled: l.enabled, lineNumber: l.line + 1, column: l.character > 0 ? l.character + 1 : 0, @@ -107,20 +106,12 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape { hitCondition: l.hitCondition } ); - const bps = await this.debugService.addBreakpoints(uri.revive(dto.uri), rawbps); - bps.forEach((bp, ix) => result.push({ - index: sdto.lines[ix].index, - id: bp.getId() - })); + this.debugService.addBreakpoints(uri.revive(dto.uri), rawbps); } else if (dto.type === 'function') { - const fbs = this.debugService.addFunctionBreakpoint(dto.functionName); - result.push({ - index: dto.index, - id: fbs.getId() - }); + this.debugService.addFunctionBreakpoint(dto.functionName, dto.id); } } - return result; + return void 0; } public $unregisterBreakpoints(breakpointIds: string[], functionBreakpointIds: string[]): TPromise { diff --git a/src/vs/workbench/api/electron-browser/mainThreadLanguageFeatures.ts b/src/vs/workbench/api/electron-browser/mainThreadLanguageFeatures.ts index 1d658c9e33aa4..8d5bf82d5051d 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadLanguageFeatures.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadLanguageFeatures.ts @@ -251,11 +251,14 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha // --- rename - $registerRenameSupport(handle: number, selector: vscode.DocumentSelector): void { + $registerRenameSupport(handle: number, selector: vscode.DocumentSelector, supportsResolveInitialValues: boolean): void { this._registrations[handle] = modes.RenameProviderRegistry.register(toLanguageSelector(selector), { provideRenameEdits: (model: ITextModel, position: EditorPosition, newName: string, token: CancellationToken): Thenable => { return wireCancellationToken(token, this._proxy.$provideRenameEdits(handle, model.uri, position, newName)).then(reviveWorkspaceEditDto); - } + }, + resolveInitialRenameValue: supportsResolveInitialValues + ? (model: ITextModel, position: EditorPosition, token: CancellationToken): Thenable => wireCancellationToken(token, this._proxy.$resolveInitialRenameValue(handle, model.uri, position)) + : undefined }); } diff --git a/src/vs/workbench/api/electron-browser/mainThreadWorkspace.ts b/src/vs/workbench/api/electron-browser/mainThreadWorkspace.ts index 08382f85d1ac8..b1cd93ae038a8 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadWorkspace.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadWorkspace.ts @@ -5,7 +5,7 @@ 'use strict'; import { isPromiseCanceledError } from 'vs/base/common/errors'; -import URI from 'vs/base/common/uri'; +import URI, { UriComponents } from 'vs/base/common/uri'; import { ISearchService, QueryType, ISearchQuery, IFolderQuery, ISearchConfiguration } from 'vs/platform/search/common/search'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; @@ -14,6 +14,9 @@ import { MainThreadWorkspaceShape, ExtHostWorkspaceShape, ExtHostContext, MainCo import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { extHostNamedCustomer } from 'vs/workbench/api/electron-browser/extHostCustomers'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IWorkspaceEditingService } from 'vs/workbench/services/workspace/common/workspaceEditing'; +import { localize } from 'vs/nls'; +import { IStatusbarService } from 'vs/platform/statusbar/common/statusbar'; @extHostNamedCustomer(MainContext.MainThreadWorkspace) export class MainThreadWorkspace implements MainThreadWorkspaceShape { @@ -27,7 +30,9 @@ export class MainThreadWorkspace implements MainThreadWorkspaceShape { @ISearchService private readonly _searchService: ISearchService, @IWorkspaceContextService private readonly _contextService: IWorkspaceContextService, @ITextFileService private readonly _textFileService: ITextFileService, - @IConfigurationService private _configurationService: IConfigurationService + @IConfigurationService private _configurationService: IConfigurationService, + @IWorkspaceEditingService private _workspaceEditingService: IWorkspaceEditingService, + @IStatusbarService private _statusbarService: IStatusbarService ) { this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostWorkspace); this._contextService.onDidChangeWorkspaceFolders(this._onDidChangeWorkspace, this, this._toDispose); @@ -45,6 +50,47 @@ export class MainThreadWorkspace implements MainThreadWorkspaceShape { // --- workspace --- + $updateWorkspaceFolders(extensionName: string, index: number, deleteCount: number, foldersToAdd: { uri: UriComponents, name?: string }[]): Thenable { + const workspaceFoldersToAdd = foldersToAdd.map(f => ({ uri: URI.revive(f.uri), name: f.name })); + + // Indicate in status message + this._statusbarService.setStatusMessage(this.getStatusMessage(extensionName, workspaceFoldersToAdd.length, deleteCount), 10 * 1000 /* 10s */); + + return this._workspaceEditingService.updateFolders(index, deleteCount, workspaceFoldersToAdd, true); + } + + private getStatusMessage(extensionName, addCount: number, removeCount: number): string { + let message: string; + + const wantsToAdd = addCount > 0; + const wantsToDelete = removeCount > 0; + + // Add Folders + if (wantsToAdd && !wantsToDelete) { + if (addCount === 1) { + message = localize('folderStatusMessageAddSingleFolder', "Extension '{0}' added 1 folder to the workspace", extensionName); + } else { + message = localize('folderStatusMessageAddMultipleFolders', "Extension '{0}' added {1} folders to the workspace", extensionName, addCount); + } + } + + // Delete Folders + else if (wantsToDelete && !wantsToAdd) { + if (removeCount === 1) { + message = localize('folderStatusMessageRemoveSingleFolder', "Extension '{0}' removed 1 folder from the workspace", extensionName); + } else { + message = localize('folderStatusMessageRemoveMultipleFolders', "Extension '{0}' removed {1} folders from the workspace", extensionName, removeCount); + } + } + + // Change Folders + else { + message = localize('folderStatusChangeFolder', "Extension '{0}' changed folders of the workspace", extensionName); + } + + return message; + } + private _onDidChangeWorkspace(): void { this._proxy.$acceptWorkspaceData(this._contextService.getWorkbenchState() === WorkbenchState.EMPTY ? null : this._contextService.getWorkspace()); } @@ -122,4 +168,4 @@ export class MainThreadWorkspace implements MainThreadWorkspaceShape { return result.results.every(each => each.success === true); }); } -} +} \ No newline at end of file diff --git a/src/vs/workbench/api/node/extHost.api.impl.ts b/src/vs/workbench/api/node/extHost.api.impl.ts index c3dc59749724f..2336cf37321cb 100644 --- a/src/vs/workbench/api/node/extHost.api.impl.ts +++ b/src/vs/workbench/api/node/extHost.api.impl.ts @@ -419,6 +419,9 @@ export function createApiFactory( set name(value) { throw errors.readonly(); }, + updateWorkspaceFolders: proposedApiFunction(extension, (index, deleteCount, ...workspaceFoldersToAdd) => { + return extHostWorkspace.updateWorkspaceFolders(extension, index, deleteCount, ...workspaceFoldersToAdd); + }), onDidChangeWorkspaceFolders: function (listener, thisArgs?, disposables?) { return extHostWorkspace.onDidChangeWorkspace(listener, thisArgs, disposables); }, diff --git a/src/vs/workbench/api/node/extHost.protocol.ts b/src/vs/workbench/api/node/extHost.protocol.ts index c98531aa15a4d..e289cf18c822f 100644 --- a/src/vs/workbench/api/node/extHost.protocol.ts +++ b/src/vs/workbench/api/node/extHost.protocol.ts @@ -277,7 +277,7 @@ export interface MainThreadLanguageFeaturesShape extends IDisposable { $registerRangeFormattingSupport(handle: number, selector: vscode.DocumentSelector): void; $registerOnTypeFormattingSupport(handle: number, selector: vscode.DocumentSelector, autoFormatTriggerCharacters: string[]): void; $registerNavigateTypeSupport(handle: number): void; - $registerRenameSupport(handle: number, selector: vscode.DocumentSelector): void; + $registerRenameSupport(handle: number, selector: vscode.DocumentSelector, supportsResolveInitialValues: boolean): void; $registerSuggestSupport(handle: number, selector: vscode.DocumentSelector, triggerCharacters: string[], supportsResolveDetails: boolean): void; $registerSignatureHelpProvider(handle: number, selector: vscode.DocumentSelector, triggerCharacter: string[]): void; $registerDocumentLinkProvider(handle: number, selector: vscode.DocumentSelector): void; @@ -349,6 +349,7 @@ export interface MainThreadWorkspaceShape extends IDisposable { $startSearch(includePattern: string, includeFolder: string, excludePattern: string, maxResults: number, requestId: number): Thenable; $cancelSearch(requestId: number): Thenable; $saveAll(includeUntitled?: boolean): Thenable; + $updateWorkspaceFolders(extensionName: string, index: number, deleteCount: number, workspaceFoldersToAdd: { uri: UriComponents, name?: string }[]): Thenable; } export interface IFileChangeDto { @@ -442,7 +443,7 @@ export interface MainThreadDebugServiceShape extends IDisposable { $customDebugAdapterRequest(id: DebugSessionUUID, command: string, args: any): TPromise; $appendDebugConsole(value: string): TPromise; $startBreakpointEvents(): TPromise; - $registerBreakpoints(breakpoints: (ISourceMultiBreakpointDto | IFunctionBreakpointDto)[]): TPromise; + $registerBreakpoints(breakpoints: (ISourceMultiBreakpointDto | IFunctionBreakpointDto)[]): TPromise; $unregisterBreakpoints(breakpointIds: string[], functionBreakpointIds: string[]): TPromise; } @@ -668,6 +669,7 @@ export interface ExtHostLanguageFeaturesShape { $resolveWorkspaceSymbol(handle: number, symbol: SymbolInformationDto): TPromise; $releaseWorkspaceSymbols(handle: number, id: number): void; $provideRenameEdits(handle: number, resource: UriComponents, position: IPosition, newName: string): TPromise; + $resolveInitialRenameValue(handle: number, resource: UriComponents, position: IPosition): TPromise; $provideCompletionItems(handle: number, resource: UriComponents, position: IPosition, context: modes.SuggestContext): TPromise; $resolveCompletionItem(handle: number, resource: UriComponents, position: IPosition, suggestion: modes.ISuggestion): TPromise; $releaseCompletionItems(handle: number, id: number): void; @@ -700,7 +702,6 @@ export interface ExtHostTaskShape { export interface IFunctionBreakpointDto { type: 'function'; - index: number; id?: string; enabled: boolean; condition?: string; @@ -729,7 +730,7 @@ export interface ISourceMultiBreakpointDto { type: 'sourceMulti'; uri: UriComponents; lines: { - index: number; + id: string; enabled: boolean; condition?: string; hitCondition?: string; @@ -738,11 +739,6 @@ export interface ISourceMultiBreakpointDto { }[]; } -export interface IBreakpointIndexDto { - index: number; - id: string; -} - export interface ExtHostDebugServiceShape { $resolveDebugConfiguration(handle: number, folder: UriComponents | undefined, debugConfiguration: IConfig): TPromise; $provideDebugConfigurations(handle: number, folder: UriComponents | undefined): TPromise; diff --git a/src/vs/workbench/api/node/extHostDebugService.ts b/src/vs/workbench/api/node/extHostDebugService.ts index feae87b4e7d72..7b1cba6a42ce6 100644 --- a/src/vs/workbench/api/node/extHostDebugService.ts +++ b/src/vs/workbench/api/node/extHostDebugService.ts @@ -16,6 +16,7 @@ import { ExtHostWorkspace } from 'vs/workbench/api/node/extHostWorkspace'; import * as vscode from 'vscode'; import URI, { UriComponents } from 'vs/base/common/uri'; import { Disposable, Position, Location, SourceBreakpoint, FunctionBreakpoint } from 'vs/workbench/api/node/extHostTypes'; +import { generateUuid } from 'vs/base/common/uuid'; export class ExtHostDebugService implements ExtHostDebugServiceShape { @@ -106,16 +107,22 @@ export class ExtHostDebugService implements ExtHostDebugServiceShape { if (delta.added) { for (const bpd of delta.added) { - let bp: vscode.Breakpoint; - if (bpd.type === 'function') { - bp = new FunctionBreakpoint(bpd.functionName, bpd.enabled, bpd.condition, bpd.hitCondition); - } else { - const uri = URI.revive(bpd.uri); - bp = new SourceBreakpoint(new Location(uri, new Position(bpd.line, bpd.character)), bpd.enabled, bpd.condition, bpd.hitCondition); + + if (!this._breakpoints.has(bpd.id)) { + let bp: vscode.Breakpoint; + if (bpd.type === 'function') { + bp = new FunctionBreakpoint(bpd.functionName, bpd.enabled, bpd.condition, bpd.hitCondition); + } else { + const uri = URI.revive(bpd.uri); + bp = new SourceBreakpoint(new Location(uri, new Position(bpd.line, bpd.character)), bpd.enabled, bpd.condition, bpd.hitCondition); + } + bp['_id'] = bpd.id; + this._breakpoints.set(bpd.id, bp); + a.push(bp); + } - bp['_id'] = bpd.id; - this._breakpoints.set(bpd.id, bp); - a.push(bp); + + } } @@ -157,25 +164,31 @@ export class ExtHostDebugService implements ExtHostDebugServiceShape { this.startBreakpoints(); - // assign temporary ids for brand new breakpoints + // assign uuids for brand new breakpoints const breakpoints: vscode.Breakpoint[] = []; for (const bp of breakpoints0) { let id = bp['_id']; if (id) { // has already id - if (!this._breakpoints.has(id)) { + if (this._breakpoints.has(id)) { + // already there + } else { breakpoints.push(bp); } } else { - // no id -> assign temp id + id = generateUuid(); + bp['_id'] = id; + this._breakpoints.set(id, bp); breakpoints.push(bp); } } - // convert to DTOs + // send notification for added breakpoints + this.fireBreakpointChanges(breakpoints, [], []); + + // convert added breakpoints to DTOs const dtos: (ISourceMultiBreakpointDto | IFunctionBreakpointDto)[] = []; const map = new Map(); - for (let i = 0; i < breakpoints.length; i++) { - const bp = breakpoints[i]; + for (const bp of breakpoints) { if (bp instanceof SourceBreakpoint) { let dto = map.get(bp.location.uri.toString()); if (!dto) { @@ -188,7 +201,7 @@ export class ExtHostDebugService implements ExtHostDebugServiceShape { dtos.push(dto); } dto.lines.push({ - index: i, + id: bp['_id'], enabled: bp.enabled, condition: bp.condition, hitCondition: bp.hitCondition, @@ -198,7 +211,7 @@ export class ExtHostDebugService implements ExtHostDebugServiceShape { } else if (bp instanceof FunctionBreakpoint) { dtos.push({ type: 'function', - index: i, + id: bp['_id'], enabled: bp.enabled, functionName: bp.functionName, hitCondition: bp.hitCondition, @@ -207,21 +220,8 @@ export class ExtHostDebugService implements ExtHostDebugServiceShape { } } - // register with VS Code - return this._debugServiceProxy.$registerBreakpoints(dtos).then(ids => { - - // assign VS Code ids to breakpoints and store them in map - ids.forEach(id => { - const bp = breakpoints[id.index]; - bp['_id'] = id.id; - this._breakpoints.set(id.id, bp); - }); - - // send notification - this.fireBreakpointChanges(breakpoints, [], []); - - return void 0; - }); + // send DTOs to VS Code + return this._debugServiceProxy.$registerBreakpoints(dtos); } public removeBreakpoints(breakpoints0: vscode.Breakpoint[]): TPromise { diff --git a/src/vs/workbench/api/node/extHostLanguageFeatures.ts b/src/vs/workbench/api/node/extHostLanguageFeatures.ts index 164f55f6ad1fd..7bf9029a45ea8 100644 --- a/src/vs/workbench/api/node/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/node/extHostLanguageFeatures.ts @@ -470,6 +470,10 @@ class NavigateTypeAdapter { class RenameAdapter { + static supportsResolving(provider: vscode.RenameProvider): boolean { + return typeof provider.resolveInitialRenameValue === 'function'; + } + private _documents: ExtHostDocuments; private _provider: vscode.RenameProvider; @@ -505,6 +509,22 @@ class RenameAdapter { } }); } + + resolveInitialRenameValue(resource: URI, position: IPosition) : TPromise { + if (typeof this._provider.resolveInitialRenameValue !== 'function') { + return TPromise.as(undefined); + } + + let doc = this._documents.getDocumentData(resource).document; + let pos = TypeConverters.toPosition(position); + + return asWinJsPromise(token => this._provider.resolveInitialRenameValue(doc, pos, token)).then((value) => { + return { + range: TypeConverters.fromRange(value.range), + text: value.text + }; + }); + } } @@ -1010,7 +1030,7 @@ export class ExtHostLanguageFeatures implements ExtHostLanguageFeaturesShape { registerRenameProvider(selector: vscode.DocumentSelector, provider: vscode.RenameProvider): vscode.Disposable { const handle = this._addNewAdapter(new RenameAdapter(this._documents, provider)); - this._proxy.$registerRenameSupport(handle, selector); + this._proxy.$registerRenameSupport(handle, selector, RenameAdapter.supportsResolving(provider)); return this._createDisposable(handle); } @@ -1018,6 +1038,10 @@ export class ExtHostLanguageFeatures implements ExtHostLanguageFeaturesShape { return this._withAdapter(handle, RenameAdapter, adapter => adapter.provideRenameEdits(URI.revive(resource), position, newName)); } + $resolveInitialRenameValue(handle: number, resource: URI, position: IPosition): TPromise { + return this._withAdapter(handle, RenameAdapter, adapter => adapter.resolveInitialRenameValue(resource, position)); + } + // --- suggestion registerCompletionItemProvider(selector: vscode.DocumentSelector, provider: vscode.CompletionItemProvider, triggerCharacters: string[]): vscode.Disposable { diff --git a/src/vs/workbench/api/node/extHostLogService.ts b/src/vs/workbench/api/node/extHostLogService.ts index 9e5dd3f93c00f..fbc0c9d308c31 100644 --- a/src/vs/workbench/api/node/extHostLogService.ts +++ b/src/vs/workbench/api/node/extHostLogService.ts @@ -4,9 +4,9 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import * as path from 'path'; import * as vscode from 'vscode'; import { TPromise } from 'vs/base/common/winjs.base'; +import { join } from 'vs/base/common/paths'; import { mkdirp, dirExists } from 'vs/base/node/pfs'; import Event from 'vs/base/common/event'; import { LogLevel } from 'vs/workbench/api/node/extHostTypes'; @@ -43,8 +43,8 @@ export class ExtHostLogService extends DelegatedLogService implements ILogServic } private createLogger(extensionID: string): ExtHostLogger { - const logService = createSpdLogService(extensionID, this.getLevel(), this._environmentService.logsPath, extensionID); - const logsDirPath = path.join(this._environmentService.logsPath, extensionID); + const logsDirPath = join(this._environmentService.logsPath, extensionID); + const logService = createSpdLogService(extensionID, this.getLevel(), logsDirPath); this._register(this.onDidChangeLogLevel(level => logService.setLevel(level))); return new ExtHostLogger(logService, logsDirPath); } diff --git a/src/vs/workbench/api/node/extHostWorkspace.ts b/src/vs/workbench/api/node/extHostWorkspace.ts index 2152866058ef3..5ead32afa25fa 100644 --- a/src/vs/workbench/api/node/extHostWorkspace.ts +++ b/src/vs/workbench/api/node/extHostWorkspace.ts @@ -7,40 +7,109 @@ import URI from 'vs/base/common/uri'; import Event, { Emitter } from 'vs/base/common/event'; import { normalize } from 'vs/base/common/paths'; -import { delta } from 'vs/base/common/arrays'; +import { delta as arrayDelta } from 'vs/base/common/arrays'; import { relative, dirname } from 'path'; import { Workspace, WorkspaceFolder } from 'vs/platform/workspace/common/workspace'; -import { IWorkspaceData, ExtHostWorkspaceShape, MainContext, MainThreadWorkspaceShape, IMainContext } from './extHost.protocol'; +import { IWorkspaceData, ExtHostWorkspaceShape, MainContext, MainThreadWorkspaceShape, IMainContext, MainThreadMessageServiceShape } from './extHost.protocol'; import * as vscode from 'vscode'; import { compare } from 'vs/base/common/strings'; import { TernarySearchTree } from 'vs/base/common/map'; +import { basenameOrAuthority, isEqual } from 'vs/base/common/resources'; +import { isLinux } from 'vs/base/common/platform'; +import { Severity } from 'vs/platform/message/common/message'; +import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { localize } from 'vs/nls'; + +function isFolderEqual(folderA: URI, folderB: URI): boolean { + return isEqual(folderA, folderB, !isLinux); +} + +function compareWorkspaceFolderByUri(a: vscode.WorkspaceFolder, b: vscode.WorkspaceFolder): number { + return isFolderEqual(a.uri, b.uri) ? 0 : compare(a.uri.toString(), b.uri.toString()); +} + +function compareWorkspaceFolderByUriAndNameAndIndex(a: vscode.WorkspaceFolder, b: vscode.WorkspaceFolder): number { + if (a.index !== b.index) { + return a.index < b.index ? -1 : 1; + } + + return isFolderEqual(a.uri, b.uri) ? compare(a.name, b.name) : compare(a.uri.toString(), b.uri.toString()); +} -class Workspace2 extends Workspace { +function delta(oldFolders: vscode.WorkspaceFolder[], newFolders: vscode.WorkspaceFolder[], compare: (a: vscode.WorkspaceFolder, b: vscode.WorkspaceFolder) => number): { removed: vscode.WorkspaceFolder[], added: vscode.WorkspaceFolder[] } { + const oldSortedFolders = oldFolders.slice(0).sort(compare); + const newSortedFolders = newFolders.slice(0).sort(compare); - static fromData(data: IWorkspaceData) { + return arrayDelta(oldSortedFolders, newSortedFolders, compare); +} + +interface MutableWorkspaceFolder extends vscode.WorkspaceFolder { + name: string; + index: number; +} + +class ExtHostWorkspaceImpl extends Workspace { + + static toExtHostWorkspace(data: IWorkspaceData, previousConfirmedWorkspace?: ExtHostWorkspaceImpl, previousUnconfirmedWorkspace?: ExtHostWorkspaceImpl): { workspace: ExtHostWorkspaceImpl, added: vscode.WorkspaceFolder[], removed: vscode.WorkspaceFolder[] } { if (!data) { - return null; + return { workspace: null, added: [], removed: [] }; + } + + const { id, name, folders } = data; + const newWorkspaceFolders: vscode.WorkspaceFolder[] = []; + + // If we have an existing workspace, we try to find the folders that match our + // data and update their properties. It could be that an extension stored them + // for later use and we want to keep them "live" if they are still present. + const oldWorkspace = previousConfirmedWorkspace; + if (oldWorkspace) { + folders.forEach((folderData, index) => { + const folderUri = URI.revive(folderData.uri); + const existingFolder = ExtHostWorkspaceImpl._findFolder(previousUnconfirmedWorkspace || previousConfirmedWorkspace, folderUri); + + if (existingFolder) { + existingFolder.name = folderData.name; + existingFolder.index = folderData.index; + + newWorkspaceFolders.push(existingFolder); + } else { + newWorkspaceFolders.push({ uri: folderUri, name: folderData.name, index }); + } + }); } else { - const { id, name, folders } = data; - return new Workspace2( - id, - name, - folders.map(({ uri, name, index }) => new WorkspaceFolder({ name, index, uri: URI.revive(uri) })) - ); + newWorkspaceFolders.push(...folders.map(({ uri, name, index }) => ({ uri: URI.revive(uri), name, index }))); + } + + // make sure to restore sort order based on index + newWorkspaceFolders.sort((f1, f2) => f1.index < f2.index ? -1 : 1); + + const workspace = new ExtHostWorkspaceImpl(id, name, newWorkspaceFolders); + const { added, removed } = delta(oldWorkspace ? oldWorkspace.workspaceFolders : [], workspace.workspaceFolders, compareWorkspaceFolderByUri); + + return { workspace, added, removed }; + } + + private static _findFolder(workspace: ExtHostWorkspaceImpl, folderUriToFind: URI): MutableWorkspaceFolder { + for (let i = 0; i < workspace.folders.length; i++) { + const folder = workspace.workspaceFolders[i]; + if (isFolderEqual(folder.uri, folderUriToFind)) { + return folder; + } } + + return undefined; } private readonly _workspaceFolders: vscode.WorkspaceFolder[] = []; private readonly _structure = TernarySearchTree.forPaths(); - private constructor(id: string, name: string, folders: WorkspaceFolder[]) { - super(id, name, folders); + private constructor(id: string, name: string, folders: vscode.WorkspaceFolder[]) { + super(id, name, folders.map(f => new WorkspaceFolder(f))); // setup the workspace folder data structure - this.folders.forEach(({ name, uri, index }) => { - const workspaceFolder = { name, uri, index }; - this._workspaceFolders.push(workspaceFolder); - this._structure.set(workspaceFolder.uri.toString(), workspaceFolder); + folders.forEach(folder => { + this._workspaceFolders.push(folder); + this._structure.set(folder.uri.toString(), folder); }); } @@ -63,44 +132,116 @@ export class ExtHostWorkspace implements ExtHostWorkspaceShape { private readonly _onDidChangeWorkspace = new Emitter(); private readonly _proxy: MainThreadWorkspaceShape; - private _workspace: Workspace2; + + private _confirmedWorkspace: ExtHostWorkspaceImpl; + private _unconfirmedWorkspace: ExtHostWorkspaceImpl; + + private _messageService: MainThreadMessageServiceShape; readonly onDidChangeWorkspace: Event = this._onDidChangeWorkspace.event; constructor(mainContext: IMainContext, data: IWorkspaceData) { this._proxy = mainContext.getProxy(MainContext.MainThreadWorkspace); - this._workspace = Workspace2.fromData(data); + this._messageService = mainContext.getProxy(MainContext.MainThreadMessageService); + this._confirmedWorkspace = ExtHostWorkspaceImpl.toExtHostWorkspace(data).workspace; } // --- workspace --- get workspace(): Workspace { - return this._workspace; + return this._actualWorkspace; + } + + private get _actualWorkspace(): ExtHostWorkspaceImpl { + return this._unconfirmedWorkspace || this._confirmedWorkspace; } getWorkspaceFolders(): vscode.WorkspaceFolder[] { - if (!this._workspace) { + if (!this._actualWorkspace) { return undefined; - } else { - return this._workspace.workspaceFolders.slice(0); } + return this._actualWorkspace.workspaceFolders.slice(0); + } + + updateWorkspaceFolders(extension: IExtensionDescription, index: number, deleteCount: number, ...workspaceFoldersToAdd: { uri: vscode.Uri, name?: string }[]): boolean { + const validatedDistinctWorkspaceFoldersToAdd: { uri: vscode.Uri, name?: string }[] = []; + if (Array.isArray(workspaceFoldersToAdd)) { + workspaceFoldersToAdd.forEach(folderToAdd => { + if (URI.isUri(folderToAdd.uri) && !validatedDistinctWorkspaceFoldersToAdd.some(f => isFolderEqual(f.uri, folderToAdd.uri))) { + validatedDistinctWorkspaceFoldersToAdd.push({ uri: folderToAdd.uri, name: folderToAdd.name || basenameOrAuthority(folderToAdd.uri) }); + } + }); + } + + if (!!this._unconfirmedWorkspace) { + return false; // prevent accumulated calls without a confirmed workspace + } + + if ([index, deleteCount].some(i => typeof i !== 'number' || i < 0)) { + return false; // validate numbers + } + + if (deleteCount === 0 && validatedDistinctWorkspaceFoldersToAdd.length === 0) { + return false; // nothing to delete or add + } + + const currentWorkspaceFolders: MutableWorkspaceFolder[] = this._actualWorkspace ? this._actualWorkspace.workspaceFolders : []; + if (index + deleteCount > currentWorkspaceFolders.length) { + return false; // cannot delete more than we have + } + + // Simulate the updateWorkspaceFolders method on our data to do more validation + const newWorkspaceFolders = currentWorkspaceFolders.slice(0); + newWorkspaceFolders.splice(index, deleteCount, ...validatedDistinctWorkspaceFoldersToAdd.map(f => ({ uri: f.uri, name: f.name || basenameOrAuthority(f.uri) }))); + + for (let i = 0; i < newWorkspaceFolders.length; i++) { + const folder = newWorkspaceFolders[i]; + if (newWorkspaceFolders.some((otherFolder, index) => index !== i && isFolderEqual(folder.uri, otherFolder.uri))) { + return false; // cannot add the same folder multiple times + } + } + + newWorkspaceFolders.forEach((f, index) => f.index = index); // fix index + const { added, removed } = delta(currentWorkspaceFolders, newWorkspaceFolders, compareWorkspaceFolderByUriAndNameAndIndex); + if (added.length === 0 && removed.length === 0) { + return false; // nothing actually changed + } + + // Trigger on main side + if (this._proxy) { + const extName = extension.displayName || extension.name; + this._proxy.$updateWorkspaceFolders(extName, index, deleteCount, validatedDistinctWorkspaceFoldersToAdd).then(null, error => { + + // in case of an error, make sure to clear out the unconfirmed workspace + // because we cannot expect the acknowledgement from the main side for this + this._unconfirmedWorkspace = undefined; + + // show error to user + this._messageService.$showMessage(Severity.Error, localize('updateerror', "Extension '{0}' failed to update workspace folders: {1}", extName, error.toString()), { extension }, []); + }); + } + + // Try to accept directly + return this.trySetWorkspaceFolders(newWorkspaceFolders); } getWorkspaceFolder(uri: vscode.Uri, resolveParent?: boolean): vscode.WorkspaceFolder { - if (!this._workspace) { + if (!this._actualWorkspace) { return undefined; } - return this._workspace.getWorkspaceFolder(uri, resolveParent); + return this._actualWorkspace.getWorkspaceFolder(uri, resolveParent); } getPath(): string { + // this is legacy from the days before having // multi-root and we keep it only alive if there // is just one workspace folder. - if (!this._workspace) { + if (!this._actualWorkspace) { return undefined; } - const { folders } = this._workspace; + + const { folders } = this._actualWorkspace; if (folders.length === 0) { return undefined; } @@ -130,7 +271,7 @@ export class ExtHostWorkspace implements ExtHostWorkspaceShape { } if (typeof includeWorkspace === 'undefined') { - includeWorkspace = this.workspace.folders.length > 1; + includeWorkspace = this._actualWorkspace.folders.length > 1; } let result = relative(folder.uri.fsPath, path); @@ -140,27 +281,40 @@ export class ExtHostWorkspace implements ExtHostWorkspaceShape { return normalize(result, true); } + private trySetWorkspaceFolders(folders: vscode.WorkspaceFolder[]): boolean { + + // Update directly here. The workspace is unconfirmed as long as we did not get an + // acknowledgement from the main side (via $acceptWorkspaceData) + if (this._actualWorkspace) { + this._unconfirmedWorkspace = ExtHostWorkspaceImpl.toExtHostWorkspace({ + id: this._actualWorkspace.id, + name: this._actualWorkspace.name, + configuration: this._actualWorkspace.configuration, + folders + } as IWorkspaceData, this._actualWorkspace).workspace; + + return true; + } + + return false; + } + $acceptWorkspaceData(data: IWorkspaceData): void { - // keep old workspace folder, build new workspace, and - // capture new workspace folders. Compute delta between - // them send that as event - const oldRoots = this._workspace ? this._workspace.workspaceFolders.sort(ExtHostWorkspace._compareWorkspaceFolder) : []; + const { workspace, added, removed } = ExtHostWorkspaceImpl.toExtHostWorkspace(data, this._confirmedWorkspace, this._unconfirmedWorkspace); - this._workspace = Workspace2.fromData(data); - const newRoots = this._workspace ? this._workspace.workspaceFolders.sort(ExtHostWorkspace._compareWorkspaceFolder) : []; + // Update our workspace object. We have a confirmed workspace, so we drop our + // unconfirmed workspace. + this._confirmedWorkspace = workspace; + this._unconfirmedWorkspace = undefined; - const { added, removed } = delta(oldRoots, newRoots, ExtHostWorkspace._compareWorkspaceFolder); + // Events this._onDidChangeWorkspace.fire(Object.freeze({ added: Object.freeze(added), removed: Object.freeze(removed) })); } - private static _compareWorkspaceFolder(a: vscode.WorkspaceFolder, b: vscode.WorkspaceFolder): number { - return compare(a.uri.toString(), b.uri.toString()); - } - // --- search --- findFiles(include: vscode.GlobPattern, exclude: vscode.GlobPattern, maxResults?: number, token?: vscode.CancellationToken): Thenable { diff --git a/src/vs/workbench/parts/debug/common/debug.ts b/src/vs/workbench/parts/debug/common/debug.ts index cffb72a71e57c..7c951a07014b1 100644 --- a/src/vs/workbench/parts/debug/common/debug.ts +++ b/src/vs/workbench/parts/debug/common/debug.ts @@ -223,6 +223,7 @@ export interface IEnablement extends ITreeElement { } export interface IRawBreakpoint { + id?: string; lineNumber: number; column?: number; enabled?: boolean; @@ -523,7 +524,7 @@ export interface IDebugService { /** * Adds new breakpoints to the model for the file specified with the uri. Notifies debug adapter of breakpoint changes. */ - addBreakpoints(uri: uri, rawBreakpoints: IRawBreakpoint[]): TPromise; + addBreakpoints(uri: uri, rawBreakpoints: IRawBreakpoint[]): TPromise; /** * Updates the breakpoints. @@ -551,7 +552,7 @@ export interface IDebugService { /** * Adds a new function breakpoint for the given name. */ - addFunctionBreakpoint(name?: string): IFunctionBreakpoint; + addFunctionBreakpoint(name?: string, id?: string): void; /** * Renames an already existing function breakpoint. diff --git a/src/vs/workbench/parts/debug/common/debugModel.ts b/src/vs/workbench/parts/debug/common/debugModel.ts index 16dd4a8a7da3c..7b162f1caab07 100644 --- a/src/vs/workbench/parts/debug/common/debugModel.ts +++ b/src/vs/workbench/parts/debug/common/debugModel.ts @@ -675,7 +675,6 @@ export class Breakpoint implements IBreakpoint { public message: string; public endLineNumber: number; public endColumn: number; - private id: string; constructor( public uri: uri, @@ -684,13 +683,13 @@ export class Breakpoint implements IBreakpoint { public enabled: boolean, public condition: string, public hitCondition: string, - public adapterData: any + public adapterData: any, + private id = generateUuid() ) { if (enabled === undefined) { this.enabled = true; } this.verified = false; - this.id = generateUuid(); } public getId(): string { @@ -700,13 +699,11 @@ export class Breakpoint implements IBreakpoint { export class FunctionBreakpoint implements IFunctionBreakpoint { - private id: string; public verified: boolean; public idFromAdapter: number; - constructor(public name: string, public enabled: boolean, public hitCondition: string) { + constructor(public name: string, public enabled: boolean, public hitCondition: string, private id = generateUuid()) { this.verified = false; - this.id = generateUuid(); } public getId(): string { @@ -867,7 +864,7 @@ export class Model implements IModel { } public addBreakpoints(uri: uri, rawData: IRawBreakpoint[], fireEvent = true): Breakpoint[] { - const newBreakpoints = rawData.map(rawBp => new Breakpoint(uri, rawBp.lineNumber, rawBp.column, rawBp.enabled, rawBp.condition, rawBp.hitCondition, undefined)); + const newBreakpoints = rawData.map(rawBp => new Breakpoint(uri, rawBp.lineNumber, rawBp.column, rawBp.enabled, rawBp.condition, rawBp.hitCondition, undefined, rawBp.id)); this.breakpoints = this.breakpoints.concat(newBreakpoints); this.breakpointsActivated = true; this.sortAndDeDup(); @@ -957,8 +954,8 @@ export class Model implements IModel { this._onDidChangeBreakpoints.fire({ changed: changed }); } - public addFunctionBreakpoint(functionName: string): FunctionBreakpoint { - const newFunctionBreakpoint = new FunctionBreakpoint(functionName, true, null); + public addFunctionBreakpoint(functionName: string, id: string): FunctionBreakpoint { + const newFunctionBreakpoint = new FunctionBreakpoint(functionName, true, null, id); this.functionBreakpoints.push(newFunctionBreakpoint); this._onDidChangeBreakpoints.fire({ added: [newFunctionBreakpoint] }); diff --git a/src/vs/workbench/parts/debug/electron-browser/debugConfigurationManager.ts b/src/vs/workbench/parts/debug/electron-browser/debugConfigurationManager.ts index 65c36f84561fd..6fe509caccb9b 100644 --- a/src/vs/workbench/parts/debug/electron-browser/debugConfigurationManager.ts +++ b/src/vs/workbench/parts/debug/electron-browser/debugConfigurationManager.ts @@ -9,7 +9,6 @@ import Event, { Emitter } from 'vs/base/common/event'; import { TPromise } from 'vs/base/common/winjs.base'; import * as strings from 'vs/base/common/strings'; import { first } from 'vs/base/common/arrays'; -import severity from 'vs/base/common/severity'; import { isLinux, isMacintosh, isWindows } from 'vs/base/common/platform'; import * as objects from 'vs/base/common/objects'; import uri from 'vs/base/common/uri'; @@ -35,7 +34,6 @@ import { IQuickOpenService } from 'vs/platform/quickOpen/common/quickOpen'; import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; import { isCodeEditor } from 'vs/editor/browser/editorBrowser'; import { launchSchemaId } from 'vs/workbench/services/configuration/common/configuration'; -import { IMessageService } from 'vs/platform/message/common/message'; // debuggers extension point export const debuggersExtPoint = extensionsRegistry.ExtensionsRegistry.registerExtensionPoint('debuggers', [], { @@ -501,8 +499,7 @@ class Launch implements ILaunch { @IFileService private fileService: IFileService, @IWorkbenchEditorService protected editorService: IWorkbenchEditorService, @IConfigurationService protected configurationService: IConfigurationService, - @IConfigurationResolverService private configurationResolverService: IConfigurationResolverService, - @IMessageService private messageService: IMessageService + @IConfigurationResolverService private configurationResolverService: IConfigurationResolverService ) { // noop } @@ -579,7 +576,7 @@ class Launch implements ILaunch { public openConfigFile(sideBySide: boolean, type?: string): TPromise { const resource = this.uri; - let configFileCreated = false; + let pinned = false; return this.fileService.resolveContent(resource).then(content => content.value, err => { @@ -599,7 +596,7 @@ class Launch implements ILaunch { return undefined; } - configFileCreated = true; + pinned = true; // pin only if config file is created #8727 return this.fileService.updateContent(resource, content).then(() => { // convert string into IContent; see #32135 return content; @@ -623,16 +620,10 @@ class Launch implements ILaunch { options: { forceOpen: true, selection, - pinned: configFileCreated, // pin only if config file is created #8727 + pinned, revealIfVisible: true }, - }, sideBySide).then(editor => { - if (configFileCreated) { - this.messageService.show(severity.Info, nls.localize('NewLaunchConfig', "Please set up the launch configuration file for your application.")); - } - - return editor; - }); + }, sideBySide); }, (error) => { throw new Error(nls.localize('DebugConfig.failed', "Unable to create 'launch.json' file inside the '.vscode' folder ({0}).", error)); }); @@ -647,10 +638,9 @@ class WorkspaceLaunch extends Launch implements ILaunch { @IWorkbenchEditorService editorService: IWorkbenchEditorService, @IConfigurationService configurationService: IConfigurationService, @IConfigurationResolverService configurationResolverService: IConfigurationResolverService, - @IWorkspaceContextService private workspaceContextService: IWorkspaceContextService, - @IMessageService messageService: IMessageService + @IWorkspaceContextService private workspaceContextService: IWorkspaceContextService ) { - super(configurationManager, undefined, fileService, editorService, configurationService, configurationResolverService, messageService); + super(configurationManager, undefined, fileService, editorService, configurationService, configurationResolverService); } get uri(): uri { diff --git a/src/vs/workbench/parts/debug/electron-browser/debugService.ts b/src/vs/workbench/parts/debug/electron-browser/debugService.ts index 04259534cc6a0..5afd2c0b37449 100644 --- a/src/vs/workbench/parts/debug/electron-browser/debugService.ts +++ b/src/vs/workbench/parts/debug/electron-browser/debugService.ts @@ -580,13 +580,11 @@ export class DebugService implements debug.IDebugService { return this.sendAllBreakpoints(); } - public addBreakpoints(uri: uri, rawBreakpoints: debug.IRawBreakpoint[]): TPromise { - const bps = this.model.addBreakpoints(uri, rawBreakpoints); + public addBreakpoints(uri: uri, rawBreakpoints: debug.IRawBreakpoint[]): TPromise { + this.model.addBreakpoints(uri, rawBreakpoints); rawBreakpoints.forEach(rbp => aria.status(nls.localize('breakpointAdded', "Added breakpoint, line {0}, file {1}", rbp.lineNumber, uri.fsPath))); - return this.sendBreakpoints(uri).then(_ => { - return bps; - }); + return this.sendBreakpoints(uri); } public updateBreakpoints(uri: uri, data: { [id: string]: DebugProtocol.Breakpoint }): void { @@ -609,10 +607,9 @@ export class DebugService implements debug.IDebugService { return this.sendAllBreakpoints(); } - public addFunctionBreakpoint(name?: string): debug.IFunctionBreakpoint { - const newFunctionBreakpoint = this.model.addFunctionBreakpoint(name || ''); + public addFunctionBreakpoint(name?: string, id?: string): void { + const newFunctionBreakpoint = this.model.addFunctionBreakpoint(name || '', id); this.viewModel.setSelectedFunctionBreakpoint(newFunctionBreakpoint); - return newFunctionBreakpoint; } public renameFunctionBreakpoint(id: string, newFunctionName: string): TPromise { diff --git a/src/vs/workbench/parts/debug/test/common/mockDebug.ts b/src/vs/workbench/parts/debug/test/common/mockDebug.ts index 737abebf93246..7d722ef668dfe 100644 --- a/src/vs/workbench/parts/debug/test/common/mockDebug.ts +++ b/src/vs/workbench/parts/debug/test/common/mockDebug.ts @@ -39,7 +39,7 @@ export class MockDebugService implements debug.IDebugService { public focusStackFrame(focusedStackFrame: debug.IStackFrame): void { } - public addBreakpoints(uri: uri, rawBreakpoints: debug.IRawBreakpoint[]): TPromise { + public addBreakpoints(uri: uri, rawBreakpoints: debug.IRawBreakpoint[]): TPromise { return TPromise.as(null); } @@ -57,9 +57,7 @@ export class MockDebugService implements debug.IDebugService { return TPromise.as(null); } - public addFunctionBreakpoint(): debug.IFunctionBreakpoint { - return null; - } + public addFunctionBreakpoint(): void { } public moveWatchExpression(id: string, position: number): void { } diff --git a/src/vs/workbench/parts/files/common/explorerModel.ts b/src/vs/workbench/parts/files/common/explorerModel.ts index 1b5c744e12203..ed348ccc0888f 100644 --- a/src/vs/workbench/parts/files/common/explorerModel.ts +++ b/src/vs/workbench/parts/files/common/explorerModel.ts @@ -7,9 +7,10 @@ import URI from 'vs/base/common/uri'; import paths = require('vs/base/common/paths'); +import resources = require('vs/base/common/resources'); import { ResourceMap } from 'vs/base/common/map'; import { isLinux } from 'vs/base/common/platform'; -import { IFileStat, isParent } from 'vs/platform/files/common/files'; +import { IFileStat } from 'vs/platform/files/common/files'; import { IEditorInput } from 'vs/platform/editor/common/editor'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IEditorGroup, toResource, IEditorIdentifier } from 'vs/workbench/common/editor'; @@ -130,7 +131,7 @@ export class FileStat implements IFileStat { // the folder is fully resolved if either it has a list of children or the client requested this by using the resolveTo // array of resource path to resolve. stat.isDirectoryResolved = !!raw.children || (!!resolveTo && resolveTo.some((r) => { - return paths.isEqualOrParent(r.fsPath, stat.resource.fsPath, !isLinux /* ignorecase */); + return resources.isEqualOrParent(r, stat.resource, !isLinux /* ignorecase */); })); // Recurse into children @@ -280,7 +281,7 @@ export class FileStat implements IFileStat { public find(resource: URI): FileStat { // Return if path found - if (paths.isEqual(resource.fsPath, this.resource.fsPath, !isLinux /* ignorecase */)) { + if (resources.isEqual(resource, this.resource, !isLinux /* ignorecase */)) { return this; } @@ -292,11 +293,11 @@ export class FileStat implements IFileStat { for (let i = 0; i < this.children.length; i++) { const child = this.children[i]; - if (paths.isEqual(resource.fsPath, child.resource.fsPath, !isLinux /* ignorecase */)) { + if (resources.isEqual(resource, child.resource, !isLinux /* ignorecase */)) { return child; } - if (child.isDirectory && isParent(resource.fsPath, child.resource.fsPath, !isLinux /* ignorecase */)) { + if (child.isDirectory && resources.isEqualOrParent(resource, child.resource, !isLinux /* ignorecase */)) { return child.find(resource); } } diff --git a/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts b/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts index ad23c7e52e904..9d28ff82fa45e 100644 --- a/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts +++ b/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts @@ -777,6 +777,16 @@ export class ExplorerView extends TreeViewsViewletPanel implements IExplorerView isDirectory: true }, root); + const setInputAndExpand = (input: FileStat | Model, statsToExpand: FileStat[]) => { + // Make sure to expand all folders that where expanded in the previous session + // Special case: we are switching to multi workspace view, thus expand all the roots (they might just be added) + if (input === this.model && statsToExpand.every(fs => !fs.isRoot)) { + statsToExpand = this.model.roots.concat(statsToExpand); + } + + return this.explorerViewer.setInput(input).then(() => this.explorerViewer.expandAll(statsToExpand)); + }; + if (targetsToResolve.every(t => t.root.resource.scheme === 'file')) { // All the roots are local, resolve them in parallel return this.fileService.resolveFiles(targetsToResolve).then(results => { @@ -792,17 +802,11 @@ export class ExplorerView extends TreeViewsViewletPanel implements IExplorerView modelStats.forEach((modelStat, index) => FileStat.mergeLocalWithDisk(modelStat, this.model.roots[index])); const statsToExpand: FileStat[] = this.explorerViewer.getExpandedElements().concat(targetsToExpand.map(expand => this.model.findClosest(expand))); - if (input === this.explorerViewer.getInput()) { - return this.explorerViewer.refresh().then(() => statsToExpand.length ? this.explorerViewer.expandAll(statsToExpand) : undefined); + return this.explorerViewer.refresh().then(() => this.explorerViewer.expandAll(statsToExpand)); } - // Make sure to expand all folders that where expanded in the previous session - // Special case: there is nothing to expand, thus expand all the roots (they might just be added) - if (statsToExpand.length === 0) { - statsToExpand.push(...this.model.roots); - } - return this.explorerViewer.setInput(input).then(() => statsToExpand.length ? this.explorerViewer.expandAll(statsToExpand) : undefined); + return setInputAndExpand(input, statsToExpand); }); } @@ -829,13 +833,7 @@ export class ExplorerView extends TreeViewsViewletPanel implements IExplorerView return delayerPromise; } - // Display roots only when multi folder workspace - // Make sure to expand all folders that where expanded in the previous session - if (input === this.model) { - // We have transitioned into workspace view -> expand all roots - toExpand = this.model.roots.concat(toExpand); - } - return this.explorerViewer.setInput(input).then(() => this.explorerViewer.expandAll(toExpand)); + return setInputAndExpand(input, statsToExpand); }))); } diff --git a/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts b/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts index 49550e507a130..b5f34511a478d 100644 --- a/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts +++ b/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts @@ -466,7 +466,7 @@ export class FileController extends DefaultController implements IDisposable { getAnchor: () => anchor, getActions: () => { const actions = []; - fillInActions(this.contributedContextMenu, { arg: stat instanceof FileStat ? stat.resource : undefined, shouldForwardArgs: true }, actions, this.contextMenuService); + fillInActions(this.contributedContextMenu, { arg: stat instanceof FileStat ? stat.resource : {}, shouldForwardArgs: true }, actions, this.contextMenuService); return TPromise.as(actions); }, onHide: (wasCancelled?: boolean) => { @@ -474,7 +474,9 @@ export class FileController extends DefaultController implements IDisposable { tree.DOMFocus(); } }, - getActionsContext: () => selection && selection.indexOf(stat) >= 0 ? selection.map((fs: FileStat) => fs.resource) : [stat] + getActionsContext: () => selection && selection.indexOf(stat) >= 0 + ? selection.map((fs: FileStat) => fs.resource) + : stat instanceof FileStat ? [stat.resource] : [] }); return true; diff --git a/src/vs/workbench/parts/logs/electron-browser/logsActions.ts b/src/vs/workbench/parts/logs/electron-browser/logsActions.ts index e9bac087e2f21..3807db0519ad2 100644 --- a/src/vs/workbench/parts/logs/electron-browser/logsActions.ts +++ b/src/vs/workbench/parts/logs/electron-browser/logsActions.ts @@ -10,7 +10,7 @@ import { IEnvironmentService } from 'vs/platform/environment/common/environment' import { IWindowsService, IWindowService } from 'vs/platform/windows/common/windows'; import { TPromise } from 'vs/base/common/winjs.base'; import { IQuickOpenService, IPickOpenEntry } from 'vs/platform/quickOpen/common/quickOpen'; -import { ILogService, LogLevel } from 'vs/platform/log/common/log'; +import { ILogService, LogLevel, DEFAULT_LOG_LEVEL } from 'vs/platform/log/common/log'; import { IOutputService, COMMAND_OPEN_LOG_VIEWER } from 'vs/workbench/parts/output/common/output'; import * as Constants from 'vs/workbench/parts/logs/common/logConstants'; import { ICommandService } from 'vs/platform/commands/common/commands'; @@ -108,14 +108,15 @@ export class SetLogLevelAction extends Action { } run(): TPromise { + const current = this.logService.getLevel(); const entries = [ - { label: nls.localize('trace', "Trace"), level: LogLevel.Trace }, - { label: nls.localize('debug', "Debug"), level: LogLevel.Debug }, - { label: nls.localize('info', "Info"), level: LogLevel.Info }, - { label: nls.localize('warn', "Warning"), level: LogLevel.Warning }, - { label: nls.localize('err', "Error"), level: LogLevel.Error }, - { label: nls.localize('critical', "Critical"), level: LogLevel.Critical }, - { label: nls.localize('off', "Off"), level: LogLevel.Off } + { label: nls.localize('trace', "Trace"), level: LogLevel.Trace, description: this.getDescription(LogLevel.Trace, current) }, + { label: nls.localize('debug', "Debug"), level: LogLevel.Debug, description: this.getDescription(LogLevel.Debug, current) }, + { label: nls.localize('info', "Info"), level: LogLevel.Info, description: this.getDescription(LogLevel.Info, current) }, + { label: nls.localize('warn', "Warning"), level: LogLevel.Warning, description: this.getDescription(LogLevel.Warning, current) }, + { label: nls.localize('err', "Error"), level: LogLevel.Error, description: this.getDescription(LogLevel.Error, current) }, + { label: nls.localize('critical', "Critical"), level: LogLevel.Critical, description: this.getDescription(LogLevel.Critical, current) }, + { label: nls.localize('off', "Off"), level: LogLevel.Off, description: this.getDescription(LogLevel.Off, current) }, ]; return this.quickOpenService.pick(entries, { placeHolder: nls.localize('selectLogLevel', "Select log level"), autoFocus: { autoFocusIndex: this.logService.getLevel() } }).then(entry => { @@ -124,4 +125,17 @@ export class SetLogLevelAction extends Action { } }); } + + private getDescription(level: LogLevel, current: LogLevel): string { + if (DEFAULT_LOG_LEVEL === level && current === level) { + return nls.localize('default and current', "Default & Current"); + } + if (DEFAULT_LOG_LEVEL === level) { + return nls.localize('default', "Default"); + } + if (current === level) { + return nls.localize('current', "Current"); + } + return void 0; + } } \ No newline at end of file diff --git a/src/vs/workbench/parts/output/electron-browser/outputServices.ts b/src/vs/workbench/parts/output/electron-browser/outputServices.ts index ce87ed605a3d8..3fdc76fcf9f25 100644 --- a/src/vs/workbench/parts/output/electron-browser/outputServices.ts +++ b/src/vs/workbench/parts/output/electron-browser/outputServices.ts @@ -40,6 +40,7 @@ import { binarySearch } from 'vs/base/common/arrays'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { Schemas } from 'vs/base/common/network'; import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; const OUTPUT_ACTIVE_CHANNEL_KEY = 'output.activechannel'; @@ -225,20 +226,25 @@ class OutputChannelBackedByFile extends AbstractFileOutputChannel implements Out private appendedMessage = ''; private loadingFromFileInProgress: boolean = false; private resettingDelayer: ThrottledDelayer; + private readonly rotatingFilePath: string; constructor( outputChannelIdentifier: IOutputChannelIdentifier, + outputDir: string, modelUri: URI, @IFileService fileService: IFileService, @IModelService modelService: IModelService, @IModeService modeService: IModeService, - @ILogService logService: ILogService + @ILogService logService: ILogService, + @IConfigurationService configurationService: IConfigurationService ) { - super(outputChannelIdentifier, modelUri, fileService, modelService, modeService); + super({ ...outputChannelIdentifier, file: URI.file(paths.join(outputDir, `${outputChannelIdentifier.id}.log`)) }, modelUri, fileService, modelService, modeService); // Use one rotating file to check for main file reset - this.outputWriter = new RotatingLogger(this.id, this.file.fsPath, 1024 * 1024 * 30, 1); + const threshold = configurationService.getValue('output.threshold'); + this.outputWriter = new RotatingLogger(this.id, this.file.fsPath, threshold && typeof threshold === 'number' ? threshold : 1024 * 1024 * 30, 1); this.outputWriter.clearFormatters(); + this.rotatingFilePath = `${outputChannelIdentifier.id}.1.log`; this._register(watchOutputDirectory(paths.dirname(this.file.fsPath), logService, (eventType, file) => this.onFileChangedInOutputDirector(eventType, file))); this.resettingDelayer = new ThrottledDelayer(50); @@ -301,7 +307,7 @@ class OutputChannelBackedByFile extends AbstractFileOutputChannel implements Out private onFileChangedInOutputDirector(eventType: string, fileName: string): void { // Check if rotating file has changed. It changes only when the main file exceeds its limit. - if (`${paths.basename(this.file.fsPath)}.1` === fileName) { + if (this.rotatingFilePath === fileName) { this.resettingDelayer.trigger(() => this.resetModel()); } } @@ -554,9 +560,8 @@ export class OutputService extends Disposable implements IOutputService, ITextMo if (channelData && channelData.file) { return this.instantiationService.createInstance(FileOutputChannel, channelData, uri); } - const file = URI.file(paths.join(this.outputDir, `${id}.log`)); try { - return this.instantiationService.createInstance(OutputChannelBackedByFile, { id, label: channelData ? channelData.label : '', file }, uri); + return this.instantiationService.createInstance(OutputChannelBackedByFile, { id, label: channelData ? channelData.label : '' }, this.outputDir, uri); } catch (e) { this.logService.error(e); this.telemetryService.publicLog('output.used.bufferedChannel'); diff --git a/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts b/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts index 6a467b92bea7e..dfdeb0359ed47 100644 --- a/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts +++ b/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts @@ -429,6 +429,7 @@ interface ResourceTemplate { fileLabel: FileLabel; decorationIcon: HTMLElement; actionBar: ActionBar; + elementDisposable: IDisposable; dispose: () => void; } @@ -464,7 +465,8 @@ class ResourceRenderer implements IRenderer { private actionItemProvider: IActionItemProvider, private getSelectedResources: () => ISCMResource[], @IThemeService private themeService: IThemeService, - @IInstantiationService private instantiationService: IInstantiationService + @IInstantiationService private instantiationService: IInstantiationService, + @IConfigurationService private configurationService: IConfigurationService ) { } renderTemplate(container: HTMLElement): ResourceTemplate { @@ -480,7 +482,7 @@ class ResourceRenderer implements IRenderer { const decorationIcon = append(element, $('.decoration-icon')); return { - element, name, fileLabel, decorationIcon, actionBar, dispose: () => { + element, name, fileLabel, decorationIcon, actionBar, elementDisposable: EmptyDisposable, dispose: () => { actionBar.dispose(); fileLabel.dispose(); } @@ -488,14 +490,22 @@ class ResourceRenderer implements IRenderer { } renderElement(resource: ISCMResource, index: number, template: ResourceTemplate): void { + template.elementDisposable.dispose(); const theme = this.themeService.getTheme(); const icon = theme.type === LIGHT ? resource.decorations.icon : resource.decorations.iconDark; template.fileLabel.setFile(resource.sourceUri, { fileDecorations: { colors: false, badges: !icon, data: resource.decorations } }); - template.actionBar.clear(); template.actionBar.context = resource; - template.actionBar.push(this.scmMenus.getResourceActions(resource), { icon: true, label: false }); + + const updateActions = () => { + template.actionBar.clear(); + template.actionBar.push(this.scmMenus.getResourceActions(resource), { icon: true, label: false }); + }; + + template.elementDisposable = this.configurationService.onDidChangeConfiguration(updateActions); + updateActions(); + toggleClass(template.name, 'strike-through', resource.decorations.strikeThrough); toggleClass(template.element, 'faded', resource.decorations.faded); @@ -507,9 +517,12 @@ class ResourceRenderer implements IRenderer { template.decorationIcon.style.display = 'none'; template.decorationIcon.style.backgroundImage = ''; } + + template.element.setAttribute('data-tooltip', resource.decorations.tooltip); } disposeTemplate(template: ResourceTemplate): void { + template.elementDisposable.dispose(); template.dispose(); } } diff --git a/src/vs/workbench/parts/update/electron-browser/update.contribution.ts b/src/vs/workbench/parts/update/electron-browser/update.contribution.ts index b533e60a755cb..6b6cc303dd52e 100644 --- a/src/vs/workbench/parts/update/electron-browser/update.contribution.ts +++ b/src/vs/workbench/parts/update/electron-browser/update.contribution.ts @@ -7,6 +7,7 @@ import * as nls from 'vs/nls'; import 'vs/css!./media/update.contribution'; +import product from 'vs/platform/node/product'; import { Registry } from 'vs/platform/registry/common/platform'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; import { ReleaseNotesEditor } from 'vs/workbench/parts/update/electron-browser/releaseNotesEditor'; @@ -60,7 +61,7 @@ configurationRegistry.registerConfiguration({ }, 'update.enableWindowsBackgroundUpdates': { 'type': 'boolean', - 'default': false, + 'default': product.quality === 'insider', 'description': nls.localize('enableWindowsBackgroundUpdates', "Enables Windows background updates.") } } diff --git a/src/vs/workbench/services/configuration/node/configurationService.ts b/src/vs/workbench/services/configuration/node/configurationService.ts index 4e3b25efb56e1..577c4f78dcdfd 100644 --- a/src/vs/workbench/services/configuration/node/configurationService.ts +++ b/src/vs/workbench/services/configuration/node/configurationService.ts @@ -110,9 +110,9 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat return this.workspace.getFolder(resource); } - public addFolders(foldersToAdd: IWorkspaceFolderCreationData[]): TPromise { + public addFolders(foldersToAdd: IWorkspaceFolderCreationData[], index?: number): TPromise { assert.ok(this.jsonEditingService, 'Workbench is not initialized yet'); - return this.workspaceEditingQueue.queue(() => this.doAddFolders(foldersToAdd)); + return this.workspaceEditingQueue.queue(() => this.doAddFolders(foldersToAdd, index)); } public removeFolders(foldersToRemove: URI[]): TPromise { @@ -134,7 +134,7 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat return false; } - private doAddFolders(foldersToAdd: IWorkspaceFolderCreationData[]): TPromise { + private doAddFolders(foldersToAdd: IWorkspaceFolderCreationData[], index?: number): TPromise { if (this.getWorkbenchState() !== WorkbenchState.WORKSPACE) { return TPromise.as(void 0); // we need a workspace to begin with } @@ -176,7 +176,16 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat }); if (storedFoldersToAdd.length > 0) { - return this.setFolders([...currentStoredFolders, ...storedFoldersToAdd]); + let newStoredWorkspaceFolders: IStoredWorkspaceFolder[] = []; + + if (typeof index === 'number' && index >= 0 && index < currentStoredFolders.length) { + newStoredWorkspaceFolders = currentStoredFolders.slice(0); + newStoredWorkspaceFolders.splice(index, 0, ...storedFoldersToAdd); + } else { + newStoredWorkspaceFolders = [...currentStoredFolders, ...storedFoldersToAdd]; + } + + return this.setFolders(newStoredWorkspaceFolders); } return TPromise.as(void 0); diff --git a/src/vs/workbench/services/configuration/test/node/configurationService.test.ts b/src/vs/workbench/services/configuration/test/node/configurationService.test.ts index 424b704b3d532..6563fd4a42c7d 100644 --- a/src/vs/workbench/services/configuration/test/node/configurationService.test.ts +++ b/src/vs/workbench/services/configuration/test/node/configurationService.test.ts @@ -192,6 +192,34 @@ suite('WorkspaceContextService - Workspace', () => { }); }); + test('add folders (at specific index)', () => { + const workspaceDir = path.dirname(testObject.getWorkspace().folders[0].uri.fsPath); + return testObject.addFolders([{ uri: URI.file(path.join(workspaceDir, 'd')) }, { uri: URI.file(path.join(workspaceDir, 'c')) }], 0) + .then(() => { + const actual = testObject.getWorkspace().folders; + + assert.equal(actual.length, 4); + assert.equal(path.basename(actual[0].uri.fsPath), 'd'); + assert.equal(path.basename(actual[1].uri.fsPath), 'c'); + assert.equal(path.basename(actual[2].uri.fsPath), 'a'); + assert.equal(path.basename(actual[3].uri.fsPath), 'b'); + }); + }); + + test('add folders (at specific wrong index)', () => { + const workspaceDir = path.dirname(testObject.getWorkspace().folders[0].uri.fsPath); + return testObject.addFolders([{ uri: URI.file(path.join(workspaceDir, 'd')) }, { uri: URI.file(path.join(workspaceDir, 'c')) }], 10) + .then(() => { + const actual = testObject.getWorkspace().folders; + + assert.equal(actual.length, 4); + assert.equal(path.basename(actual[0].uri.fsPath), 'a'); + assert.equal(path.basename(actual[1].uri.fsPath), 'b'); + assert.equal(path.basename(actual[2].uri.fsPath), 'd'); + assert.equal(path.basename(actual[3].uri.fsPath), 'c'); + }); + }); + test('add folders (with name)', () => { const workspaceDir = path.dirname(testObject.getWorkspace().folders[0].uri.fsPath); return testObject.addFolders([{ uri: URI.file(path.join(workspaceDir, 'd')), name: 'DDD' }, { uri: URI.file(path.join(workspaceDir, 'c')), name: 'CCC' }]) diff --git a/src/vs/workbench/services/workspace/common/workspaceEditing.ts b/src/vs/workbench/services/workspace/common/workspaceEditing.ts index 3e22bf28ba644..920be3ef66792 100644 --- a/src/vs/workbench/services/workspace/common/workspaceEditing.ts +++ b/src/vs/workbench/services/workspace/common/workspaceEditing.ts @@ -27,6 +27,12 @@ export interface IWorkspaceEditingService { */ removeFolders(folders: URI[], donotNotifyError?: boolean): TPromise; + /** + * Allows to add and remove folders to the existing workspace at once. + * When `donotNotifyError` is `true`, error will be bubbled up otherwise, the service handles the error with proper message and action + */ + updateFolders(index: number, deleteCount?: number, foldersToAdd?: IWorkspaceFolderCreationData[], donotNotifyError?: boolean): TPromise; + /** * creates a new workspace with the provided folders and opens it. if path is provided * the workspace will be saved into that location. diff --git a/src/vs/workbench/services/workspace/node/workspaceEditingService.ts b/src/vs/workbench/services/workspace/node/workspaceEditingService.ts index 7bb21530f427e..c35254fc87fe5 100644 --- a/src/vs/workbench/services/workspace/node/workspaceEditingService.ts +++ b/src/vs/workbench/services/workspace/node/workspaceEditingService.ts @@ -47,16 +47,56 @@ export class WorkspaceEditingService implements IWorkspaceEditingService { ) { } + public updateFolders(index: number, deleteCount?: number, foldersToAdd?: IWorkspaceFolderCreationData[], donotNotifyError?: boolean): TPromise { + const folders = this.contextService.getWorkspace().folders; + + let foldersToDelete: URI[] = []; + if (typeof deleteCount === 'number') { + foldersToDelete = folders.slice(index, index + deleteCount).map(f => f.uri); + } + + const wantsToDelete = foldersToDelete.length > 0; + const wantsToAdd = Array.isArray(foldersToAdd) && foldersToAdd.length > 0; + + if (!wantsToAdd && !wantsToDelete) { + return TPromise.as(void 0); // return early if there is nothing to do + } + + // Add Folders + if (wantsToAdd && !wantsToDelete) { + return this.doAddFolders(foldersToAdd, index, donotNotifyError); + } + + // Delete Folders + if (wantsToDelete && !wantsToAdd) { + return this.removeFolders(foldersToDelete); + } + + // Add & Delete Folders + if (this.includesSingleFolderWorkspace(foldersToDelete)) { + // if we are in single-folder state and the folder is replaced with + // other folders, we handle this specially and just enter workspace + // mode with the folders that are being added. + return this.createAndEnterWorkspace(foldersToAdd); + } + + // Make sure to first remove folders and then add them to account for folders being updated + return this.removeFolders(foldersToDelete).then(() => this.doAddFolders(foldersToAdd, index, donotNotifyError)); + } + public addFolders(foldersToAdd: IWorkspaceFolderCreationData[], donotNotifyError: boolean = false): TPromise { + return this.doAddFolders(foldersToAdd, void 0, donotNotifyError); + } + + private doAddFolders(foldersToAdd: IWorkspaceFolderCreationData[], index?: number, donotNotifyError: boolean = false): TPromise { const state = this.contextService.getWorkbenchState(); // If we are in no-workspace or single-folder workspace, adding folders has to // enter a workspace. if (state !== WorkbenchState.WORKSPACE) { - const newWorkspaceFolders: IWorkspaceFolderCreationData[] = distinct([ - ...this.contextService.getWorkspace().folders.map(folder => ({ uri: folder.uri } as IWorkspaceFolderCreationData)), - ...foldersToAdd - ] as IWorkspaceFolderCreationData[], folder => isLinux ? folder.uri.toString() : folder.uri.toString().toLowerCase()); + let newWorkspaceFolders = this.contextService.getWorkspace().folders.map(folder => ({ uri: folder.uri } as IWorkspaceFolderCreationData)); + newWorkspaceFolders.splice(typeof index === 'number' ? index : newWorkspaceFolders.length, 0, ...foldersToAdd); + newWorkspaceFolders = distinct(newWorkspaceFolders, folder => isLinux ? folder.uri.toString() : folder.uri.toString().toLowerCase()); if (state === WorkbenchState.EMPTY && newWorkspaceFolders.length === 0 || state === WorkbenchState.FOLDER && newWorkspaceFolders.length === 1) { return TPromise.as(void 0); // return if the operation is a no-op for the current state @@ -66,19 +106,16 @@ export class WorkspaceEditingService implements IWorkspaceEditingService { } // Delegate addition of folders to workspace service otherwise - return this.contextService.addFolders(foldersToAdd) + return this.contextService.addFolders(foldersToAdd, index) .then(() => null, error => donotNotifyError ? TPromise.wrapError(error) : this.handleWorkspaceConfigurationEditingError(error)); } public removeFolders(foldersToRemove: URI[], donotNotifyError: boolean = false): TPromise { // If we are in single-folder state and the opened folder is to be removed, - // we close the workspace and enter the empty workspace state for the window. - if (this.contextService.getWorkbenchState() === WorkbenchState.FOLDER) { - const workspaceFolder = this.contextService.getWorkspace().folders[0]; - if (foldersToRemove.some(folder => isEqual(folder, workspaceFolder.uri, !isLinux))) { - return this.windowService.closeWorkspace(); - } + // we create an empty workspace and enter it. + if (this.includesSingleFolderWorkspace(foldersToRemove)) { + return this.createAndEnterWorkspace([]); } // Delegate removal of folders to workspace service otherwise @@ -86,6 +123,15 @@ export class WorkspaceEditingService implements IWorkspaceEditingService { .then(() => null, error => donotNotifyError ? TPromise.wrapError(error) : this.handleWorkspaceConfigurationEditingError(error)); } + private includesSingleFolderWorkspace(folders: URI[]): boolean { + if (this.contextService.getWorkbenchState() === WorkbenchState.FOLDER) { + const workspaceFolder = this.contextService.getWorkspace().folders[0]; + return (folders.some(folder => isEqual(folder, workspaceFolder.uri, !isLinux))); + } + + return false; + } + public createAndEnterWorkspace(folders?: IWorkspaceFolderCreationData[], path?: string): TPromise { return this.doEnterWorkspace(() => this.windowService.createAndEnterWorkspace(folders, path)); } diff --git a/src/vs/workbench/test/electron-browser/api/extHostWorkspace.test.ts b/src/vs/workbench/test/electron-browser/api/extHostWorkspace.test.ts index 75f5cb5effeb4..8a56c49a9e7e3 100644 --- a/src/vs/workbench/test/electron-browser/api/extHostWorkspace.test.ts +++ b/src/vs/workbench/test/electron-browser/api/extHostWorkspace.test.ts @@ -12,9 +12,21 @@ import { ExtHostWorkspace } from 'vs/workbench/api/node/extHostWorkspace'; import { TestRPCProtocol } from './testRPCProtocol'; import { normalize } from 'vs/base/common/paths'; import { IWorkspaceFolderData } from 'vs/platform/workspace/common/workspace'; +import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; suite('ExtHostWorkspace', function () { + const extensionDescriptor: IExtensionDescription = { + id: 'nullExtensionDescription', + name: 'ext', + publisher: 'vscode', + enableProposedApi: false, + engines: undefined, + extensionFolderPath: undefined, + isBuiltin: false, + version: undefined + }; + function assertAsRelativePath(workspace: ExtHostWorkspace, input: string, expected: string, includeWorkspace?: boolean) { const actual = workspace.getRelativePath(input, includeWorkspace); if (actual === expected) { @@ -159,57 +171,370 @@ suite('ExtHostWorkspace', function () { assert.equal(folder.name, 'Two'); }); - test('Multiroot change event should have a delta, #29641', function () { + test('Multiroot change event should have a delta, #29641', function (done) { let ws = new ExtHostWorkspace(new TestRPCProtocol(), { id: 'foo', name: 'Test', folders: [] }); + let finished = false; + const finish = (error?) => { + if (!finished) { + finished = true; + done(error); + } + }; + let sub = ws.onDidChangeWorkspace(e => { - assert.deepEqual(e.added, []); - assert.deepEqual(e.removed, []); + try { + assert.deepEqual(e.added, []); + assert.deepEqual(e.removed, []); + } catch (error) { + finish(error); + } }); ws.$acceptWorkspaceData({ id: 'foo', name: 'Test', folders: [] }); sub.dispose(); sub = ws.onDidChangeWorkspace(e => { - assert.deepEqual(e.removed, []); - assert.equal(e.added.length, 1); - assert.equal(e.added[0].uri.toString(), 'foo:bar'); + try { + assert.deepEqual(e.removed, []); + assert.equal(e.added.length, 1); + assert.equal(e.added[0].uri.toString(), 'foo:bar'); + } catch (error) { + finish(error); + } }); ws.$acceptWorkspaceData({ id: 'foo', name: 'Test', folders: [aWorkspaceFolderData(URI.parse('foo:bar'), 0)] }); sub.dispose(); sub = ws.onDidChangeWorkspace(e => { - assert.deepEqual(e.removed, []); - assert.equal(e.added.length, 1); - assert.equal(e.added[0].uri.toString(), 'foo:bar2'); + try { + assert.deepEqual(e.removed, []); + assert.equal(e.added.length, 1); + assert.equal(e.added[0].uri.toString(), 'foo:bar2'); + } catch (error) { + finish(error); + } }); ws.$acceptWorkspaceData({ id: 'foo', name: 'Test', folders: [aWorkspaceFolderData(URI.parse('foo:bar'), 0), aWorkspaceFolderData(URI.parse('foo:bar2'), 1)] }); sub.dispose(); sub = ws.onDidChangeWorkspace(e => { - assert.equal(e.removed.length, 2); - assert.equal(e.removed[0].uri.toString(), 'foo:bar'); - assert.equal(e.removed[1].uri.toString(), 'foo:bar2'); - - assert.equal(e.added.length, 1); - assert.equal(e.added[0].uri.toString(), 'foo:bar3'); + try { + assert.equal(e.removed.length, 2); + assert.equal(e.removed[0].uri.toString(), 'foo:bar'); + assert.equal(e.removed[1].uri.toString(), 'foo:bar2'); + + assert.equal(e.added.length, 1); + assert.equal(e.added[0].uri.toString(), 'foo:bar3'); + } catch (error) { + finish(error); + } }); ws.$acceptWorkspaceData({ id: 'foo', name: 'Test', folders: [aWorkspaceFolderData(URI.parse('foo:bar3'), 0)] }); sub.dispose(); + finish(); + }); + + test('Multiroot change keeps existing workspaces live', function () { + let ws = new ExtHostWorkspace(new TestRPCProtocol(), { id: 'foo', name: 'Test', folders: [aWorkspaceFolderData(URI.parse('foo:bar'), 0)] }); + + let firstFolder = ws.getWorkspaceFolders()[0]; + ws.$acceptWorkspaceData({ id: 'foo', name: 'Test', folders: [aWorkspaceFolderData(URI.parse('foo:bar2'), 0), aWorkspaceFolderData(URI.parse('foo:bar'), 1, 'renamed')] }); + + assert.equal(ws.getWorkspaceFolders()[1], firstFolder); + assert.equal(firstFolder.index, 1); + assert.equal(firstFolder.name, 'renamed'); + + ws.$acceptWorkspaceData({ id: 'foo', name: 'Test', folders: [aWorkspaceFolderData(URI.parse('foo:bar3'), 0), aWorkspaceFolderData(URI.parse('foo:bar2'), 1), aWorkspaceFolderData(URI.parse('foo:bar'), 2)] }); + assert.equal(ws.getWorkspaceFolders()[2], firstFolder); + assert.equal(firstFolder.index, 2); + + ws.$acceptWorkspaceData({ id: 'foo', name: 'Test', folders: [aWorkspaceFolderData(URI.parse('foo:bar3'), 0)] }); + ws.$acceptWorkspaceData({ id: 'foo', name: 'Test', folders: [aWorkspaceFolderData(URI.parse('foo:bar3'), 0), aWorkspaceFolderData(URI.parse('foo:bar'), 1)] }); + + assert.notEqual(firstFolder, ws.workspace.folders[0]); + }); + + test('updateWorkspaceFolders - invalid arguments', function () { + let ws = new ExtHostWorkspace(new TestRPCProtocol(), { id: 'foo', name: 'Test', folders: [] }); + + assert.equal(false, ws.updateWorkspaceFolders(extensionDescriptor, null, null)); + assert.equal(false, ws.updateWorkspaceFolders(extensionDescriptor, 0, 0)); + assert.equal(false, ws.updateWorkspaceFolders(extensionDescriptor, 0, 1)); + assert.equal(false, ws.updateWorkspaceFolders(extensionDescriptor, 1, 0)); + assert.equal(false, ws.updateWorkspaceFolders(extensionDescriptor, -1, 0)); + assert.equal(false, ws.updateWorkspaceFolders(extensionDescriptor, -1, -1)); + + ws = new ExtHostWorkspace(new TestRPCProtocol(), { id: 'foo', name: 'Test', folders: [aWorkspaceFolderData(URI.parse('foo:bar'), 0)] }); + + assert.equal(false, ws.updateWorkspaceFolders(extensionDescriptor, 1, 1)); + assert.equal(false, ws.updateWorkspaceFolders(extensionDescriptor, 0, 2)); + assert.equal(false, ws.updateWorkspaceFolders(extensionDescriptor, 0, 1, asUpdateWorkspaceFolderData(URI.parse('foo:bar')))); + }); + + test('updateWorkspaceFolders - valid arguments', function (done) { + let finished = false; + const finish = (error?) => { + if (!finished) { + finished = true; + done(error); + } + }; + + const protocol = { + getProxy: () => { return undefined; }, + set: undefined, + assertRegistered: undefined + }; + + const ws = new ExtHostWorkspace(protocol, { id: 'foo', name: 'Test', folders: [] }); + + // + // Add one folder + // + + assert.equal(true, ws.updateWorkspaceFolders(extensionDescriptor, 0, 0, asUpdateWorkspaceFolderData(URI.parse('foo:bar')))); + assert.equal(1, ws.workspace.folders.length); + assert.equal(ws.workspace.folders[0].uri.toString(), URI.parse('foo:bar').toString()); + + const firstAddedFolder = ws.getWorkspaceFolders()[0]; + + let gotEvent = false; + let sub = ws.onDidChangeWorkspace(e => { + try { + assert.deepEqual(e.removed, []); + assert.equal(e.added.length, 1); + assert.equal(e.added[0].uri.toString(), 'foo:bar'); + assert.equal(e.added[0], firstAddedFolder); // verify object is still live + gotEvent = true; + } catch (error) { + finish(error); + } + }); + ws.$acceptWorkspaceData({ id: 'foo', name: 'Test', folders: [aWorkspaceFolderData(URI.parse('foo:bar'), 0)] }); // simulate acknowledgement from main side + assert.equal(gotEvent, true); + sub.dispose(); + assert.equal(ws.getWorkspaceFolders()[0], firstAddedFolder); // verify object is still live + + // + // Add two more folders + // + + assert.equal(true, ws.updateWorkspaceFolders(extensionDescriptor, 1, 0, asUpdateWorkspaceFolderData(URI.parse('foo:bar1')), asUpdateWorkspaceFolderData(URI.parse('foo:bar2')))); + assert.equal(3, ws.workspace.folders.length); + assert.equal(ws.workspace.folders[0].uri.toString(), URI.parse('foo:bar').toString()); + assert.equal(ws.workspace.folders[1].uri.toString(), URI.parse('foo:bar1').toString()); + assert.equal(ws.workspace.folders[2].uri.toString(), URI.parse('foo:bar2').toString()); + + const secondAddedFolder = ws.getWorkspaceFolders()[1]; + const thirdAddedFolder = ws.getWorkspaceFolders()[2]; + + gotEvent = false; + sub = ws.onDidChangeWorkspace(e => { + try { + assert.deepEqual(e.removed, []); + assert.equal(e.added.length, 2); + assert.equal(e.added[0].uri.toString(), 'foo:bar1'); + assert.equal(e.added[1].uri.toString(), 'foo:bar2'); + assert.equal(e.added[0], secondAddedFolder); + assert.equal(e.added[1], thirdAddedFolder); + gotEvent = true; + } catch (error) { + finish(error); + } + }); + ws.$acceptWorkspaceData({ id: 'foo', name: 'Test', folders: [aWorkspaceFolderData(URI.parse('foo:bar'), 0), aWorkspaceFolderData(URI.parse('foo:bar1'), 1), aWorkspaceFolderData(URI.parse('foo:bar2'), 2)] }); // simulate acknowledgement from main side + assert.equal(gotEvent, true); + sub.dispose(); + assert.equal(ws.getWorkspaceFolders()[0], firstAddedFolder); // verify object is still live + assert.equal(ws.getWorkspaceFolders()[1], secondAddedFolder); // verify object is still live + assert.equal(ws.getWorkspaceFolders()[2], thirdAddedFolder); // verify object is still live + + // + // Remove one folder + // + + assert.equal(true, ws.updateWorkspaceFolders(extensionDescriptor, 2, 1)); + assert.equal(2, ws.workspace.folders.length); + assert.equal(ws.workspace.folders[0].uri.toString(), URI.parse('foo:bar').toString()); + assert.equal(ws.workspace.folders[1].uri.toString(), URI.parse('foo:bar1').toString()); + + gotEvent = false; + sub = ws.onDidChangeWorkspace(e => { + try { + assert.deepEqual(e.added, []); + assert.equal(e.removed.length, 1); + assert.equal(e.removed[0], thirdAddedFolder); + gotEvent = true; + } catch (error) { + finish(error); + } + }); + ws.$acceptWorkspaceData({ id: 'foo', name: 'Test', folders: [aWorkspaceFolderData(URI.parse('foo:bar'), 0), aWorkspaceFolderData(URI.parse('foo:bar1'), 1)] }); // simulate acknowledgement from main side + assert.equal(gotEvent, true); + sub.dispose(); + assert.equal(ws.getWorkspaceFolders()[0], firstAddedFolder); // verify object is still live + assert.equal(ws.getWorkspaceFolders()[1], secondAddedFolder); // verify object is still live + + // + // Rename folder + // + + assert.equal(true, ws.updateWorkspaceFolders(extensionDescriptor, 0, 2, asUpdateWorkspaceFolderData(URI.parse('foo:bar'), 'renamed 1'), asUpdateWorkspaceFolderData(URI.parse('foo:bar1'), 'renamed 2'))); + assert.equal(2, ws.workspace.folders.length); + assert.equal(ws.workspace.folders[0].uri.toString(), URI.parse('foo:bar').toString()); + assert.equal(ws.workspace.folders[1].uri.toString(), URI.parse('foo:bar1').toString()); + assert.equal(ws.workspace.folders[0].name, 'renamed 1'); + assert.equal(ws.workspace.folders[1].name, 'renamed 2'); + assert.equal(ws.getWorkspaceFolders()[0].name, 'renamed 1'); + assert.equal(ws.getWorkspaceFolders()[1].name, 'renamed 2'); + + gotEvent = false; + sub = ws.onDidChangeWorkspace(e => { + try { + assert.deepEqual(e.added, []); + assert.equal(e.removed.length, []); + gotEvent = true; + } catch (error) { + finish(error); + } + }); + ws.$acceptWorkspaceData({ id: 'foo', name: 'Test', folders: [aWorkspaceFolderData(URI.parse('foo:bar'), 0, 'renamed 1'), aWorkspaceFolderData(URI.parse('foo:bar1'), 1, 'renamed 2')] }); // simulate acknowledgement from main side + assert.equal(gotEvent, true); + sub.dispose(); + assert.equal(ws.getWorkspaceFolders()[0], firstAddedFolder); // verify object is still live + assert.equal(ws.getWorkspaceFolders()[1], secondAddedFolder); // verify object is still live + assert.equal(ws.workspace.folders[0].name, 'renamed 1'); + assert.equal(ws.workspace.folders[1].name, 'renamed 2'); + assert.equal(ws.getWorkspaceFolders()[0].name, 'renamed 1'); + assert.equal(ws.getWorkspaceFolders()[1].name, 'renamed 2'); + + // + // Add and remove folders + // + + assert.equal(true, ws.updateWorkspaceFolders(extensionDescriptor, 0, 2, asUpdateWorkspaceFolderData(URI.parse('foo:bar3')), asUpdateWorkspaceFolderData(URI.parse('foo:bar4')))); + assert.equal(2, ws.workspace.folders.length); + assert.equal(ws.workspace.folders[0].uri.toString(), URI.parse('foo:bar3').toString()); + assert.equal(ws.workspace.folders[1].uri.toString(), URI.parse('foo:bar4').toString()); + + const fourthAddedFolder = ws.getWorkspaceFolders()[0]; + const fifthAddedFolder = ws.getWorkspaceFolders()[1]; + + gotEvent = false; + sub = ws.onDidChangeWorkspace(e => { + try { + assert.equal(e.added.length, 2); + assert.equal(e.added[0], fourthAddedFolder); + assert.equal(e.added[1], fifthAddedFolder); + assert.equal(e.removed.length, 2); + assert.equal(e.removed[0], firstAddedFolder); + assert.equal(e.removed[1], secondAddedFolder); + gotEvent = true; + } catch (error) { + finish(error); + } + }); + ws.$acceptWorkspaceData({ id: 'foo', name: 'Test', folders: [aWorkspaceFolderData(URI.parse('foo:bar3'), 0), aWorkspaceFolderData(URI.parse('foo:bar4'), 1)] }); // simulate acknowledgement from main side + assert.equal(gotEvent, true); + sub.dispose(); + assert.equal(ws.getWorkspaceFolders()[0], fourthAddedFolder); // verify object is still live + assert.equal(ws.getWorkspaceFolders()[1], fifthAddedFolder); // verify object is still live + + // + // Swap folders + // + + assert.equal(true, ws.updateWorkspaceFolders(extensionDescriptor, 0, 2, asUpdateWorkspaceFolderData(URI.parse('foo:bar4')), asUpdateWorkspaceFolderData(URI.parse('foo:bar3')))); + assert.equal(2, ws.workspace.folders.length); + assert.equal(ws.workspace.folders[0].uri.toString(), URI.parse('foo:bar4').toString()); + assert.equal(ws.workspace.folders[1].uri.toString(), URI.parse('foo:bar3').toString()); + + assert.equal(ws.getWorkspaceFolders()[0], fifthAddedFolder); // verify object is still live + assert.equal(ws.getWorkspaceFolders()[1], fourthAddedFolder); // verify object is still live + + gotEvent = false; + sub = ws.onDidChangeWorkspace(e => { + try { + assert.equal(e.added.length, 0); + assert.equal(e.removed.length, 0); + gotEvent = true; + } catch (error) { + finish(error); + } + }); + ws.$acceptWorkspaceData({ id: 'foo', name: 'Test', folders: [aWorkspaceFolderData(URI.parse('foo:bar4'), 0), aWorkspaceFolderData(URI.parse('foo:bar3'), 1)] }); // simulate acknowledgement from main side + assert.equal(gotEvent, true); + sub.dispose(); + assert.equal(ws.getWorkspaceFolders()[0], fifthAddedFolder); // verify object is still live + assert.equal(ws.getWorkspaceFolders()[1], fourthAddedFolder); // verify object is still live + assert.equal(fifthAddedFolder.index, 0); + assert.equal(fourthAddedFolder.index, 1); + + // + // Add one folder after the other without waiting for confirmation (not supported currently) + // + + assert.equal(true, ws.updateWorkspaceFolders(extensionDescriptor, 2, 0, asUpdateWorkspaceFolderData(URI.parse('foo:bar5')))); + + assert.equal(3, ws.workspace.folders.length); + assert.equal(ws.workspace.folders[0].uri.toString(), URI.parse('foo:bar4').toString()); + assert.equal(ws.workspace.folders[1].uri.toString(), URI.parse('foo:bar3').toString()); + assert.equal(ws.workspace.folders[2].uri.toString(), URI.parse('foo:bar5').toString()); + + const sixthAddedFolder = ws.getWorkspaceFolders()[2]; + + gotEvent = false; + sub = ws.onDidChangeWorkspace(e => { + try { + assert.equal(e.added.length, 1); + assert.equal(e.added[0], sixthAddedFolder); + gotEvent = true; + } catch (error) { + finish(error); + } + }); + ws.$acceptWorkspaceData({ + id: 'foo', name: 'Test', folders: [ + aWorkspaceFolderData(URI.parse('foo:bar4'), 0), + aWorkspaceFolderData(URI.parse('foo:bar3'), 1), + aWorkspaceFolderData(URI.parse('foo:bar5'), 2) + ] + }); // simulate acknowledgement from main side + assert.equal(gotEvent, true); + sub.dispose(); + + assert.equal(ws.getWorkspaceFolders()[0], fifthAddedFolder); // verify object is still live + assert.equal(ws.getWorkspaceFolders()[1], fourthAddedFolder); // verify object is still live + assert.equal(ws.getWorkspaceFolders()[2], sixthAddedFolder); // verify object is still live + finish(); }); - test('Multiroot change event is immutable', function () { + test('Multiroot change event is immutable', function (done) { + let finished = false; + const finish = (error?) => { + if (!finished) { + finished = true; + done(error); + } + }; + let ws = new ExtHostWorkspace(new TestRPCProtocol(), { id: 'foo', name: 'Test', folders: [] }); let sub = ws.onDidChangeWorkspace(e => { - assert.throws(() => { - (e).added = []; - }); - assert.throws(() => { - (e.added)[0] = null; - }); + try { + assert.throws(() => { + (e).added = []; + }); + assert.throws(() => { + (e.added)[0] = null; + }); + } catch (error) { + finish(error); + } }); ws.$acceptWorkspaceData({ id: 'foo', name: 'Test', folders: [] }); sub.dispose(); + finish(); }); test('`vscode.workspace.getWorkspaceFolder(file)` don\'t return workspace folder when file open from command line. #36221', function () { @@ -230,4 +555,8 @@ suite('ExtHostWorkspace', function () { name: name || basename(uri.path) }; } + + function asUpdateWorkspaceFolderData(uri: URI, name?: string): { uri: URI, name?: string } { + return { uri, name }; + } }); diff --git a/test/smoke/src/areas/debug/debug.test.ts b/test/smoke/src/areas/debug/debug.test.ts index 3218911858dc8..7e924b95b626e 100644 --- a/test/smoke/src/areas/debug/debug.test.ts +++ b/test/smoke/src/areas/debug/debug.test.ts @@ -5,54 +5,19 @@ import * as assert from 'assert'; import * as http from 'http'; -import * as os from 'os'; import * as path from 'path'; import * as fs from 'fs'; import * as stripJsonComments from 'strip-json-comments'; -import { SpectronApplication, Quality } from '../../spectron/application'; +import { SpectronApplication } from '../../spectron/application'; export function setup() { describe('Debug', () => { - let skip = false; - before(async function () { const app = this.app as SpectronApplication; - - if (app.quality === Quality.Dev) { - const extensionsPath = path.join(os.homedir(), '.vscode-oss-dev', 'extensions'); - - const debugPath = path.join(extensionsPath, 'vscode-node-debug'); - const debugExists = fs.existsSync(debugPath); - - const debug2Path = path.join(extensionsPath, 'vscode-node-debug2'); - const debug2Exists = fs.existsSync(debug2Path); - - if (!debugExists) { - console.warn(`Skipping debug tests because vscode-node-debug extension was not found in ${extensionsPath}`); - skip = true; - return; - } - - if (!debug2Exists) { - console.warn(`Skipping debug tests because vscode-node-debug2 extension was not found in ${extensionsPath}`); - skip = true; - return; - } - - await new Promise((c, e) => fs.symlink(debugPath, path.join(app.extensionsPath, 'vscode-node-debug'), err => err ? e(err) : c())); - await new Promise((c, e) => fs.symlink(debug2Path, path.join(app.extensionsPath, 'vscode-node-debug2'), err => err ? e(err) : c())); - await app.reload(); - } - - this.app.suiteName = 'Debug'; + app.suiteName = 'Debug'; }); it('configure launch json', async function () { - if (skip) { - this.skip(); - return; - } - const app = this.app as SpectronApplication; await app.workbench.debug.openDebugViewlet(); @@ -78,11 +43,6 @@ export function setup() { }); it('breakpoints', async function () { - if (skip) { - this.skip(); - return; - } - const app = this.app as SpectronApplication; await app.workbench.quickopen.openFile('index.js'); @@ -92,11 +52,6 @@ export function setup() { let port: number; it('start debugging', async function () { - if (skip) { - this.skip(); - return; - } - const app = this.app as SpectronApplication; port = await app.workbench.debug.startDebugging(); @@ -112,11 +67,6 @@ export function setup() { }); it('focus stack frames and variables', async function () { - if (skip) { - this.skip(); - return; - } - const app = this.app as SpectronApplication; await app.client.waitFor(() => app.workbench.debug.getLocalVariableCount(), c => c === 4, 'there should be 4 local variables'); @@ -132,11 +82,6 @@ export function setup() { }); it('stepOver, stepIn, stepOut', async function () { - if (skip) { - this.skip(); - return; - } - const app = this.app as SpectronApplication; await app.workbench.debug.stepIn(); @@ -154,11 +99,6 @@ export function setup() { }); it('continue', async function () { - if (skip) { - this.skip(); - return; - } - const app = this.app as SpectronApplication; await app.workbench.debug.continue(); @@ -174,22 +114,12 @@ export function setup() { }); it('debug console', async function () { - if (skip) { - this.skip(); - return; - } - const app = this.app as SpectronApplication; await app.workbench.debug.waitForReplCommand('2 + 2', r => r === '4'); }); it('stop debugging', async function () { - if (skip) { - this.skip(); - return; - } - const app = this.app as SpectronApplication; await app.workbench.debug.stopDebugging(); diff --git a/test/smoke/src/areas/git/git.test.ts b/test/smoke/src/areas/git/git.test.ts index 19f1c3e51d274..7ce6407053f63 100644 --- a/test/smoke/src/areas/git/git.test.ts +++ b/test/smoke/src/areas/git/git.test.ts @@ -12,8 +12,9 @@ const SYNC_STATUSBAR = 'div[id="workbench.parts.statusbar"] .statusbar-entry a[t export function setup() { describe('Git', () => { - before(function () { - this.app.suiteName = 'Git'; + before(async function () { + const app = this.app as SpectronApplication; + app.suiteName = 'Git'; }); it('reflects working tree changes', async function () { diff --git a/test/smoke/src/areas/git/scm.ts b/test/smoke/src/areas/git/scm.ts index ed33e320710d9..824cfc3d144af 100644 --- a/test/smoke/src/areas/git/scm.ts +++ b/test/smoke/src/areas/git/scm.ts @@ -13,7 +13,7 @@ const SCM_RESOURCE = `${VIEWLET} .monaco-list-row > .resource`; const SCM_RESOURCE_GROUP = `${VIEWLET} .monaco-list-row > .resource-group`; const REFRESH_COMMAND = `div[id="workbench.parts.sidebar"] .actions-container a.action-label[title="Refresh"]`; const COMMIT_COMMAND = `div[id="workbench.parts.sidebar"] .actions-container a.action-label[title="Commit"]`; -const SCM_RESOURCE_CLICK = name => `${SCM_RESOURCE} .monaco-icon-label[title*="${name}"]`; +const SCM_RESOURCE_CLICK = name => `${SCM_RESOURCE} .monaco-icon-label[title*="${name}"] .label-name`; const SCM_RESOURCE_GROUP_COMMAND_CLICK = name => `${SCM_RESOURCE_GROUP} .actions .action-label[title="${name}"]`; export interface Change { @@ -49,7 +49,7 @@ export class SCM extends Viewlet { const result = await this.spectron.webclient.selectorExecute(SCM_RESOURCE, div => (Array.isArray(div) ? div : [div]).map(element => { const name = element.querySelector('.label-name') as HTMLElement; - const icon = element.querySelector('.decoration-icon') as HTMLElement; + const type = element.getAttribute('data-tooltip') || ''; const actionElementList = element.querySelectorAll('.actions .action-label'); const actionElements: any[] = []; @@ -60,7 +60,7 @@ export class SCM extends Viewlet { return { name: name.textContent, - type: (icon.title || ''), + type, element, actionElements }; diff --git a/test/smoke/src/areas/workbench/workbench.ts b/test/smoke/src/areas/workbench/workbench.ts index 08a3204d1ebd7..c2711f449d347 100644 --- a/test/smoke/src/areas/workbench/workbench.ts +++ b/test/smoke/src/areas/workbench/workbench.ts @@ -51,14 +51,8 @@ export class Workbench { } public async saveOpenedFile(): Promise { - try { - await this.spectron.client.waitForElement('.tabs-container div.tab.active.dirty'); - } catch (e) { - // ignore if there is no dirty file - return Promise.resolve(); - } - await this.spectron.runCommand('workbench.action.files.save'); - return this.spectron.client.waitForElement('.tabs-container div.tab.active.dirty', element => !element); + await this.spectron.client.waitForElement('.tabs-container div.tab.active.dirty'); + await this.spectron.workbench.quickopen.runCommand('File: Save'); } public async selectTab(tabName: string, untitled: boolean = false): Promise { diff --git a/test/smoke/src/main.ts b/test/smoke/src/main.ts index 1d922106600ba..019ee28cff607 100644 --- a/test/smoke/src/main.ts +++ b/test/smoke/src/main.ts @@ -220,7 +220,7 @@ console.warn = function suppressWebdriverWarnings(message) { }; function createApp(quality: Quality): SpectronApplication | null { - const path = quality === Quality.Insiders ? electronPath : stablePath; + const path = quality === Quality.Stable ? stablePath : electronPath; if (!path) { return null; diff --git a/yarn.lock b/yarn.lock index a13d19e090de8..190ecd187fa67 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5213,9 +5213,9 @@ sparkles@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/sparkles/-/sparkles-1.0.0.tgz#1acbbfb592436d10bbe8f785b7cc6f82815012c3" -spdlog@0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/spdlog/-/spdlog-0.5.0.tgz#5ec92c34e59f29328f4e19dfab17a1ba51cc0573" +spdlog@0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/spdlog/-/spdlog-0.6.0.tgz#20632ed4f1558ffa46e8a5827a5e97c61e0fa9ed" dependencies: bindings "^1.3.0" mkdirp "^0.5.1" @@ -5977,8 +5977,8 @@ vscode-fsevents@0.3.8: nan "^2.3.0" vscode-nls-dev@^3.0.5: - version "3.0.5" - resolved "https://registry.yarnpkg.com/vscode-nls-dev/-/vscode-nls-dev-3.0.5.tgz#f3ffd04852abfba7db9a615697084023fbceceb2" + version "3.0.6" + resolved "https://registry.yarnpkg.com/vscode-nls-dev/-/vscode-nls-dev-3.0.6.tgz#8d07a74b09763df0cf10175b8588ed0d7aa52664" dependencies: clone "^2.1.1" event-stream "^3.3.4"