Merge remote-tracking branch 'origin/master' into alex/asar

This commit is contained in:
Alex Dima 2018-01-26 17:37:23 +01:00
commit 2970cf5517
77 changed files with 1789 additions and 596 deletions

18
.vscode/launch.json vendored
View file

@ -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"

View file

@ -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"
},
{

View file

@ -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'
];

View file

@ -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`));

View file

@ -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') {
if (project === extensionsProject) {
var extPack = extensionsPacks[resource];
if (!extPack) {
extPack = extensionsPacks[resource] = { version: i18nPackVersion, contents: {} };
}
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);
}
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;
}
});
});

View file

@ -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"

View file

@ -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<V> {
[key: string]: V;
}
@ -193,10 +202,12 @@ class TextModel {
export class XLF {
private buffer: string[];
private files: Map<Item[]>;
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<string>();
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<string[]> {
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<string[]> = 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<boolean> {
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<I18nPack> = {};
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;
}
});
}

View file

@ -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) {

View file

@ -60,26 +60,26 @@ export class SettingsDocument {
private provideFilesAssociationsCompletionItems(location: Location, range: vscode.Range): vscode.ProviderResult<vscode.CompletionItem[]> {
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
}));
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
}));
}
// Value
else if (location.path.length === 2 && !location.isAtPropertyKey) {
return this.provideLanguageCompletionItems(location, 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);

View file

@ -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"
}
}
}

View file

@ -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."
}
}

View file

@ -963,26 +963,30 @@ export class CommandCenter {
getCommitMessage: () => Promise<string | undefined>,
opts?: CommitOptions
): Promise<boolean> {
const unsavedTextDocuments = workspace.textDocuments
.filter(d => !d.isUntitled && d.isDirty && isDescendant(repository.root, d.uri.fsPath));
const config = workspace.getConfiguration('git');
const promptToSaveFilesBeforeCommit = config.get<boolean>('promptToSaveFilesBeforeCommit') === true;
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 (promptToSaveFilesBeforeCommit) {
const unsavedTextDocuments = workspace.textDocuments
.filter(d => !d.isUntitled && d.isDirty && isDescendant(repository.root, d.uri.fsPath));
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
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<boolean>('enableSmartCommit') === true;
const enableCommitSigning = config.get<boolean>('enableCommitSigning') === true;
const noStagedChanges = repository.indexGroup.resourceStates.length === 0;

View file

@ -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', () => {

View file

@ -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();

View file

@ -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', () => {

View file

@ -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 {

View file

@ -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;

View file

@ -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')));
}
});
});

View file

@ -0,0 +1 @@
Just a simple file...

View file

@ -0,0 +1,11 @@
{
"folders": [
{
"path": "testWorkspace"
},
{
"path": "testWorkspace2",
"name": "Test Workspace 2"
}
]
}

View file

@ -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",

View file

@ -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%

View file

@ -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

View file

@ -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,

View file

@ -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,

View file

@ -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<WorkspaceEdit>;
resolveInitialRenameValue?(model: model.ITextModel, position: Position, token: CancellationToken): RenameInitialValue | Thenable<RenameInitialValue>;
}

View file

@ -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<RenameInitialValue> {
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<RenameInitialValue>(new Error('provider failed'));
});
}
// --- register actions and commands
@ -116,34 +129,64 @@ class RenameController implements IEditorContribution {
return RenameController.ID;
}
public run(): TPromise<void> {
const selection = this.editor.getSelection(),
word = this.editor.getModel().getWordAtPosition(selection.getStartPosition());
if (!word) {
return undefined;
}
public async run(): TPromise<void> {
const selection = this.editor.getSelection();
let lineNumber = selection.startLineNumber,
selectionStart = 0,
selectionEnd = word.word.length,
wordRange: Range;
selectionEnd = 0,
wordRange: Range,
word: string;
wordRange = new Range(
lineNumber,
word.startColumn,
lineNumber,
word.endColumn
);
let initialValue = await resolveInitialRenameValue(this.editor.getModel(), this.editor.getPosition());
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(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
);
}
else {
const wordAtPosition = this.editor.getModel().getWordAtPosition(selection.getStartPosition());
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 => {

View file

@ -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') {

View file

@ -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<string, SuggestMemory>();
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);
export interface MemItem {
type: string;
insertText: string;
touch: number;
}
export class LRUMemory extends Memory {
private _cache = new LRUCache<string, MemItem>(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({ 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);
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<MemItem>();
private _seq = 0;
private readonly _memory = new LRUCache<string, MemoryItem>(400, 0.75);
constructor(tuples?: [string, MemoryItem][]) {
if (tuples) {
for (const [word, item] of tuples) {
this._memory.set(word, item);
}
}
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++
});
}
remember(item: ICompletionItem): void {
if (item.word) {
this._memory.set(item.word, { insertText: item.suggestion.insertText, type: item.suggestion.type });
select(model: ITextModel, pos: IPosition, items: ICompletionItem[]): number {
let { word } = model.getWordUntilPosition(pos);
if (!word) {
return 0;
}
}
select(items: ICompletionItem[], last: ICompletionItem): number {
if (items.length === 0) {
return -1;
let key = `${model.getLanguageIdentifier().language}/${word}`;
let item = this._trie.get(key);
if (!item) {
item = this._trie.findSubstr(key);
}
const topScore = items[0].score;
for (let i = 0; i < items.length; i++) {
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;
}
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])) {
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 -1;
return 0;
}
private _matches(item: MemoryItem, candidate: ICompletionItem): boolean {
return item && item.insertText === candidate.suggestion.insertText && item.type === candidate.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);
}
toJSON(): [string, MemoryItem][] {
const tuples: [string, MemoryItem][] = [];
this._memory.forEach((value, key) => tuples.push([key, value]));
return tuples;
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);
}
}
}
}
export type MemMode = 'never' | 'byRecency' | 'byPrefix';
export class SuggestMemories {
private readonly _storagePrefix = 'suggest/memories';
private _mode: MemMode;
private _strategy: Memory;
private _persistSoon: RunOnceScheduler;
constructor(
mode: MemMode,
@IStorageService private _storageService: IStorageService
) {
this._persistSoon = new RunOnceScheduler(() => this._flush(), 3000);
this.setMode(mode);
}
setMode(mode: MemMode): void {
if (this._mode === mode) {
return;
}
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();
}
select(model: ITextModel, pos: IPosition, items: ICompletionItem[]): number {
return this._strategy.select(model, pos, items);
}
private _flush() {
const raw = JSON.stringify(this._strategy);
this._storageService.store(`${this._storagePrefix}/${this._mode}`, raw, StorageScope.WORKSPACE);
}
}

View file

@ -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 () {
export function createSuggestItem(label: string, overwriteBefore: number, type: SuggestionType = 'property', incomplete: boolean = false, position: IPosition = { lineNumber: 1, column: 1 }): ISuggestionItem {
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
};
suggestion: ISuggestion = {
label,
overwriteBefore,
insertText: label,
type
};
container: ISuggestResult = {
incomplete,
suggestions: [this.suggestion]
};
container: ISuggestResult = {
incomplete,
suggestions: [this.suggestion]
};
support: ISuggestSupport = {
provideCompletionItems(): any {
return;
}
};
resolve(): TPromise<void> {
return null;
support: ISuggestSupport = {
provideCompletionItems(): any {
return;
}
};
}
resolve(): TPromise<void> {
return null;
}
};
}
suite('CompletionModel', function () {
let model: CompletionModel;

View file

@ -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
});
});

11
src/vs/monaco.d.ts vendored
View file

@ -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<WorkspaceEdit>;
resolveInitialRenameValue?(model: editor.ITextModel, position: Position, token: CancellationToken): RenameInitialValue | Thenable<RenameInitialValue>;
}
export interface Command {

View file

@ -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;

View file

@ -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);

View file

@ -331,9 +331,12 @@ suite('ExtensionEnablementService Test', () => {
});
function aLocalExtension(id: string, contributes?: IExtensionContributions): ILocalExtension {
const [publisher, name] = id.split('.');
return <ILocalExtension>Object.create({
identifier: { id },
manifest: {
name,
publisher,
contributes
}
});

View file

@ -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<LogLevel>;
@ -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<LogLevel> = this._register(new Emitter<LogLevel>());
readonly onDidChangeLogLevel: Event<LogLevel> = 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;
}

View file

@ -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);

View file

@ -98,6 +98,7 @@ const configurationValueWhitelist = [
'editor.snippetSuggestions',
'editor.emptySelectionClipboard',
'editor.wordBasedSuggestions',
'editor.selectSuggestions',
'editor.suggestFontSize',
'editor.suggestLineHeight',
'editor.selectionHighlight',

View file

@ -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) {

7
src/vs/vscode.d.ts vendored
View file

@ -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<WorkspaceEdit>;
resolveInitialRenameValue?(document: TextDocument, position: Position, token: CancellationToken): ProviderResult<RenameInitialValue>;
}
/**

View file

@ -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<void>;
export function addBreakpoints(breakpoints: Breakpoint[]): void;
/**
* Remove breakpoints.
* @param breakpoints The breakpoints to remove.
*/
export function removeBreakpoints(breakpoints: Breakpoint[]): Thenable<void>;
export function removeBreakpoints(breakpoints: Breakpoint[]): void;
}
/**

View file

@ -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<void>(undefined);
}
public async $registerBreakpoints(DTOs: (ISourceMultiBreakpointDto | IFunctionBreakpointDto)[]): TPromise<IBreakpointIndexDto[]> {
public $registerBreakpoints(DTOs: (ISourceMultiBreakpointDto | IFunctionBreakpointDto)[]): TPromise<void> {
const result: IBreakpointIndexDto[] = [];
for (let dto of DTOs) {
if (dto.type === 'sourceMulti') {
const sdto = <ISourceMultiBreakpointDto>dto;
const rawbps = dto.lines.map(l =>
<IRawBreakpoint>{
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<void> {

View file

@ -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), <modes.RenameProvider>{
provideRenameEdits: (model: ITextModel, position: EditorPosition, newName: string, token: CancellationToken): Thenable<modes.WorkspaceEdit> => {
return wireCancellationToken(token, this._proxy.$provideRenameEdits(handle, model.uri, position, newName)).then(reviveWorkspaceEditDto);
}
},
resolveInitialRenameValue: supportsResolveInitialValues
? (model: ITextModel, position: EditorPosition, token: CancellationToken): Thenable<modes.RenameInitialValue> => wireCancellationToken(token, this._proxy.$resolveInitialRenameValue(handle, model.uri, position))
: undefined
});
}

View file

@ -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<void> {
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);
});
}
}
}

View file

@ -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);
},

View file

@ -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<UriComponents[]>;
$cancelSearch(requestId: number): Thenable<boolean>;
$saveAll(includeUntitled?: boolean): Thenable<boolean>;
$updateWorkspaceFolders(extensionName: string, index: number, deleteCount: number, workspaceFoldersToAdd: { uri: UriComponents, name?: string }[]): Thenable<void>;
}
export interface IFileChangeDto {
@ -442,7 +443,7 @@ export interface MainThreadDebugServiceShape extends IDisposable {
$customDebugAdapterRequest(id: DebugSessionUUID, command: string, args: any): TPromise<any>;
$appendDebugConsole(value: string): TPromise<any>;
$startBreakpointEvents(): TPromise<any>;
$registerBreakpoints(breakpoints: (ISourceMultiBreakpointDto | IFunctionBreakpointDto)[]): TPromise<IBreakpointIndexDto[]>;
$registerBreakpoints(breakpoints: (ISourceMultiBreakpointDto | IFunctionBreakpointDto)[]): TPromise<void>;
$unregisterBreakpoints(breakpointIds: string[], functionBreakpointIds: string[]): TPromise<void>;
}
@ -668,6 +669,7 @@ export interface ExtHostLanguageFeaturesShape {
$resolveWorkspaceSymbol(handle: number, symbol: SymbolInformationDto): TPromise<SymbolInformationDto>;
$releaseWorkspaceSymbols(handle: number, id: number): void;
$provideRenameEdits(handle: number, resource: UriComponents, position: IPosition, newName: string): TPromise<WorkspaceEditDto>;
$resolveInitialRenameValue(handle: number, resource: UriComponents, position: IPosition): TPromise<modes.RenameInitialValue>;
$provideCompletionItems(handle: number, resource: UriComponents, position: IPosition, context: modes.SuggestContext): TPromise<SuggestResultDto>;
$resolveCompletionItem(handle: number, resource: UriComponents, position: IPosition, suggestion: modes.ISuggestion): TPromise<modes.ISuggestion>;
$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<IConfig>;
$provideDebugConfigurations(handle: number, folder: UriComponents | undefined): TPromise<IConfig[]>;

View file

@ -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<string, ISourceMultiBreakpointDto>();
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<void> {

View file

@ -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<modes.RenameInitialValue> {
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 <modes.RenameInitialValue> {
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<modes.RenameInitialValue> {
return this._withAdapter(handle, RenameAdapter, adapter => adapter.resolveInitialRenameValue(resource, position));
}
// --- suggestion
registerCompletionItemProvider(selector: vscode.DocumentSelector, provider: vscode.CompletionItemProvider, triggerCharacters: string[]): vscode.Disposable {

View file

@ -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);
}

View file

@ -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';
class Workspace2 extends Workspace {
function isFolderEqual(folderA: URI, folderB: URI): boolean {
return isEqual(folderA, folderB, !isLinux);
}
static fromData(data: IWorkspaceData) {
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());
}
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);
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;
} else {
const { id, name, folders } = data;
return new Workspace2(
id,
name,
folders.map(({ uri, name, index }) => new WorkspaceFolder({ name, index, uri: URI.revive(uri) }))
);
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 {
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<vscode.WorkspaceFolder>();
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<vscode.WorkspaceFoldersChangeEvent>();
private readonly _proxy: MainThreadWorkspaceShape;
private _workspace: Workspace2;
private _confirmedWorkspace: ExtHostWorkspaceImpl;
private _unconfirmedWorkspace: ExtHostWorkspaceImpl;
private _messageService: MainThreadMessageServiceShape;
readonly onDidChangeWorkspace: Event<vscode.WorkspaceFoldersChangeEvent> = 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<vscode.WorkspaceFolder[]>(added),
removed: Object.freeze<vscode.WorkspaceFolder[]>(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<vscode.Uri[]> {

View file

@ -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<IBreakpoint[]>;
addBreakpoints(uri: uri, rawBreakpoints: IRawBreakpoint[]): TPromise<void>;
/**
* 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.

View file

@ -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] });

View file

@ -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<IRawAdapter[]>('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<IEditor> {
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 {

View file

@ -580,13 +580,11 @@ export class DebugService implements debug.IDebugService {
return this.sendAllBreakpoints();
}
public addBreakpoints(uri: uri, rawBreakpoints: debug.IRawBreakpoint[]): TPromise<debug.IBreakpoint[]> {
const bps = this.model.addBreakpoints(uri, rawBreakpoints);
public addBreakpoints(uri: uri, rawBreakpoints: debug.IRawBreakpoint[]): TPromise<void> {
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<void> {

View file

@ -39,7 +39,7 @@ export class MockDebugService implements debug.IDebugService {
public focusStackFrame(focusedStackFrame: debug.IStackFrame): void {
}
public addBreakpoints(uri: uri, rawBreakpoints: debug.IRawBreakpoint[]): TPromise<debug.IBreakpoint[]> {
public addBreakpoints(uri: uri, rawBreakpoints: debug.IRawBreakpoint[]): TPromise<void> {
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 { }

View file

@ -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);
}
}

View file

@ -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);
})));
}

View file

@ -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;

View file

@ -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<void> {
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;
}
}

View file

@ -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<void>;
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<void>(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');

View file

@ -429,6 +429,7 @@ interface ResourceTemplate {
fileLabel: FileLabel;
decorationIcon: HTMLElement;
actionBar: ActionBar;
elementDisposable: IDisposable;
dispose: () => void;
}
@ -464,7 +465,8 @@ class ResourceRenderer implements IRenderer<ISCMResource, ResourceTemplate> {
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<ISCMResource, ResourceTemplate> {
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<ISCMResource, ResourceTemplate> {
}
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<ISCMResource, ResourceTemplate> {
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();
}
}

View file

@ -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.")
}
}

View file

@ -110,9 +110,9 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat
return this.workspace.getFolder(resource);
}
public addFolders(foldersToAdd: IWorkspaceFolderCreationData[]): TPromise<void> {
public addFolders(foldersToAdd: IWorkspaceFolderCreationData[], index?: number): TPromise<void> {
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<void> {
@ -134,7 +134,7 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat
return false;
}
private doAddFolders(foldersToAdd: IWorkspaceFolderCreationData[]): TPromise<void> {
private doAddFolders(foldersToAdd: IWorkspaceFolderCreationData[], index?: number): TPromise<void> {
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);

View file

@ -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' }])

View file

@ -27,6 +27,12 @@ export interface IWorkspaceEditingService {
*/
removeFolders(folders: URI[], donotNotifyError?: boolean): TPromise<void>;
/**
* 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<void>;
/**
* creates a new workspace with the provided folders and opens it. if path is provided
* the workspace will be saved into that location.

View file

@ -47,16 +47,56 @@ export class WorkspaceEditingService implements IWorkspaceEditingService {
) {
}
public updateFolders(index: number, deleteCount?: number, foldersToAdd?: IWorkspaceFolderCreationData[], donotNotifyError?: boolean): TPromise<void> {
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<void> {
return this.doAddFolders(foldersToAdd, void 0, donotNotifyError);
}
private doAddFolders(foldersToAdd: IWorkspaceFolderCreationData[], index?: number, donotNotifyError: boolean = false): TPromise<void> {
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<void> {
// 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<void> {
return this.doEnterWorkspace(() => this.windowService.createAndEnterWorkspace(folders, path));
}

View file

@ -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');
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');
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 event is immutable', function () {
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 (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(() => {
(<any>e).added = [];
});
assert.throws(() => {
(<any>e.added)[0] = null;
});
try {
assert.throws(() => {
(<any>e).added = [];
});
assert.throws(() => {
(<any>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 };
}
});

View file

@ -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();

View file

@ -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 () {

View file

@ -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
};

View file

@ -51,14 +51,8 @@ export class Workbench {
}
public async saveOpenedFile(): Promise<any> {
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<void> {

View file

@ -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;

View file

@ -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"