diff --git a/.eslintrc.json b/.eslintrc.json index 3b8d12542e5..d95ee4630c3 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -227,6 +227,7 @@ "@vscode/vscode-languagedetection", "@vscode/ripgrep", "@vscode/iconv-lite-umd", + "@vscode/policy-watcher", "assert", "child_process", "console", @@ -254,7 +255,6 @@ "url", "util", "v8-inspect-profiler", - "vscode-policy-watcher", "vscode-proxy-agent", "vscode-regexpp", "vscode-textmate", diff --git a/.vscode/notebooks/endgame.github-issues b/.vscode/notebooks/endgame.github-issues index d3b3ae08fa5..4a61518494e 100644 --- a/.vscode/notebooks/endgame.github-issues +++ b/.vscode/notebooks/endgame.github-issues @@ -7,7 +7,7 @@ { "kind": 2, "language": "github-issues", - "value": "$REPOS=repo:microsoft/vscode repo:microsoft/vscode-remote-release repo:microsoft/vscode-js-debug repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-dev repo:microsoft/vscode-unpkg repo:microsoft/vscode-references-view repo:microsoft/vscode-anycode repo:microsoft/vscode-hexeditor repo:microsoft/vscode-extension-telemetry repo:microsoft/vscode-livepreview repo:microsoft/vscode-remotehub repo:microsoft/vscode-settings-sync-server repo:microsoft/vscode-remote-repositories-github repo:microsoft/monaco-editor repo:microsoft/vscode-vsce repo:microsoft/vscode-dev-chrome-launcher repo:microsoft/vscode-emmet-helper repo:microsoft/vscode-livepreview repo:microsoft/vscode-livepreview repo:microsoft/vscode-python repo:microsoft/vscode-jupyter repo:microsoft/vscode-jupyter-internal repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-l10n repo:microsoft/vscode-remote-tunnels\n\n$MILESTONE=milestone:\"January 2023\"" + "value": "$REPOS=repo:microsoft/vscode repo:microsoft/vscode-remote-release repo:microsoft/vscode-js-debug repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-dev repo:microsoft/vscode-unpkg repo:microsoft/vscode-references-view repo:microsoft/vscode-anycode repo:microsoft/vscode-hexeditor repo:microsoft/vscode-extension-telemetry repo:microsoft/vscode-livepreview repo:microsoft/vscode-remotehub repo:microsoft/vscode-settings-sync-server repo:microsoft/vscode-remote-repositories-github repo:microsoft/monaco-editor repo:microsoft/vscode-vsce repo:microsoft/vscode-dev-chrome-launcher repo:microsoft/vscode-emmet-helper repo:microsoft/vscode-livepreview repo:microsoft/vscode-livepreview repo:microsoft/vscode-python repo:microsoft/vscode-jupyter repo:microsoft/vscode-jupyter-internal repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-l10n repo:microsoft/vscode-remote-tunnels\n\n$MILESTONE=milestone:\"February 2023\"" }, { "kind": 1, diff --git a/.vscode/notebooks/my-endgame.github-issues b/.vscode/notebooks/my-endgame.github-issues index f1c838cdeb7..57e276cb7f2 100644 --- a/.vscode/notebooks/my-endgame.github-issues +++ b/.vscode/notebooks/my-endgame.github-issues @@ -7,7 +7,7 @@ { "kind": 2, "language": "github-issues", - "value": "$REPOS=repo:microsoft/vscode repo:microsoft/vscode-remote-release repo:microsoft/vscode-js-debug repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-dev repo:microsoft/vscode-unpkg repo:microsoft/vscode-references-view repo:microsoft/vscode-anycode repo:microsoft/vscode-hexeditor repo:microsoft/vscode-extension-telemetry repo:microsoft/vscode-livepreview repo:microsoft/vscode-remotehub repo:microsoft/vscode-settings-sync-server repo:microsoft/vscode-remote-repositories-github repo:microsoft/monaco-editor repo:microsoft/vscode-vsce repo:microsoft/vscode-dev-chrome-launcher repo:microsoft/vscode-emmet-helper repo:microsoft/vscode-livepreview repo:microsoft/vscode-livepreview repo:microsoft/vscode-python repo:microsoft/vscode-jupyter repo:microsoft/vscode-jupyter-internal repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-l10n repo:microsoft/vscode-remote-tunnels\n\n$MILESTONE=milestone:\"January 2023\"\n\n$MINE=assignee:@me" + "value": "$REPOS=repo:microsoft/vscode repo:microsoft/vscode-remote-release repo:microsoft/vscode-js-debug repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-dev repo:microsoft/vscode-unpkg repo:microsoft/vscode-references-view repo:microsoft/vscode-anycode repo:microsoft/vscode-hexeditor repo:microsoft/vscode-extension-telemetry repo:microsoft/vscode-livepreview repo:microsoft/vscode-remotehub repo:microsoft/vscode-settings-sync-server repo:microsoft/vscode-remote-repositories-github repo:microsoft/monaco-editor repo:microsoft/vscode-vsce repo:microsoft/vscode-dev-chrome-launcher repo:microsoft/vscode-emmet-helper repo:microsoft/vscode-livepreview repo:microsoft/vscode-livepreview repo:microsoft/vscode-python repo:microsoft/vscode-jupyter repo:microsoft/vscode-jupyter-internal repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-l10n repo:microsoft/vscode-remote-tunnels\n\n$MILESTONE=milestone:\"February 2023\"\n\n$MINE=assignee:@me" }, { "kind": 1, diff --git a/.vscode/notebooks/verification.github-issues b/.vscode/notebooks/verification.github-issues index b4ad043d9bf..15cf60b17dd 100644 --- a/.vscode/notebooks/verification.github-issues +++ b/.vscode/notebooks/verification.github-issues @@ -12,7 +12,7 @@ { "kind": 2, "language": "github-issues", - "value": "$repos=repo:microsoft/vscode repo:microsoft/vscode-remote-release repo:microsoft/vscode-js-debug repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-dev repo:microsoft/vscode-unpkg repo:microsoft/vscode-references-view repo:microsoft/vscode-anycode repo:microsoft/vscode-hexeditor repo:microsoft/vscode-extension-telemetry repo:microsoft/vscode-livepreview repo:microsoft/vscode-remotehub repo:microsoft/vscode-settings-sync-server repo:microsoft/vscode-remote-repositories-github repo:microsoft/monaco-editor repo:microsoft/vscode-vsce repo:microsoft/vscode-dev-chrome-launcher repo:microsoft/vscode-emmet-helper repo:microsoft/vscode-livepreview repo:microsoft/vscode-livepreview repo:microsoft/vscode-python repo:microsoft/vscode-jupyter repo:microsoft/vscode-jupyter-internal repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-l10n repo:microsoft/vscode-remote-tunnels\n$milestone=milestone:\"November 2022\"" + "value": "$repos=repo:microsoft/vscode repo:microsoft/vscode-remote-release repo:microsoft/vscode-js-debug repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-dev repo:microsoft/vscode-unpkg repo:microsoft/vscode-references-view repo:microsoft/vscode-anycode repo:microsoft/vscode-hexeditor repo:microsoft/vscode-extension-telemetry repo:microsoft/vscode-livepreview repo:microsoft/vscode-remotehub repo:microsoft/vscode-settings-sync-server repo:microsoft/vscode-remote-repositories-github repo:microsoft/monaco-editor repo:microsoft/vscode-vsce repo:microsoft/vscode-dev-chrome-launcher repo:microsoft/vscode-emmet-helper repo:microsoft/vscode-livepreview repo:microsoft/vscode-livepreview repo:microsoft/vscode-python repo:microsoft/vscode-jupyter repo:microsoft/vscode-jupyter-internal repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-l10n repo:microsoft/vscode-remote-tunnels\n$milestone=milestone:\"February 2023\"" }, { "kind": 1, diff --git a/build/.moduleignore b/build/.moduleignore index e2b045acc34..7b57188f137 100644 --- a/build/.moduleignore +++ b/build/.moduleignore @@ -113,13 +113,13 @@ vscode-encrypt/binding.gyp vscode-encrypt/README.md !vscode-encrypt/build/Release/vscode-encrypt-native.node -vscode-policy-watcher/build/** -vscode-policy-watcher/.husky/** -vscode-policy-watcher/src/** -vscode-policy-watcher/binding.gyp -vscode-policy-watcher/README.md -vscode-policy-watcher/index.d.ts -!vscode-policy-watcher/build/Release/vscode-policy-watcher.node +@vscode/policy-watcher/build/** +@vscode/policy-watcher/.husky/** +@vscode/policy-watcher/src/** +@vscode/policy-watcher/binding.gyp +@vscode/policy-watcher/README.md +@vscode/policy-watcher/index.d.ts +!@vscode/policy-watcher/build/Release/vscode-policy-watcher.node vscode-windows-ca-certs/**/* !vscode-windows-ca-certs/package.json diff --git a/build/azure-pipelines/product-compile.yml b/build/azure-pipelines/product-compile.yml index e02eaad1bac..fe1f55c6f27 100644 --- a/build/azure-pipelines/product-compile.yml +++ b/build/azure-pipelines/product-compile.yml @@ -177,4 +177,5 @@ steps: displayName: "Component Detection" inputs: sourceScanPath: $(Build.SourcesDirectory) + alertWarningLevel: 'Medium' continueOnError: true diff --git a/build/azure-pipelines/upload-cdn.js b/build/azure-pipelines/upload-cdn.js index 287d2b0e403..5a218735d3f 100644 --- a/build/azure-pipelines/upload-cdn.js +++ b/build/azure-pipelines/upload-cdn.js @@ -9,51 +9,109 @@ const Vinyl = require("vinyl"); const vfs = require("vinyl-fs"); const filter = require("gulp-filter"); const gzip = require("gulp-gzip"); +const mime = require("mime"); const identity_1 = require("@azure/identity"); const azure = require('gulp-azure-storage'); const commit = process.env['VSCODE_DISTRO_COMMIT'] || process.env['BUILD_SOURCEVERSION']; const credential = new identity_1.ClientSecretCredential(process.env['AZURE_TENANT_ID'], process.env['AZURE_CLIENT_ID'], process.env['AZURE_CLIENT_SECRET']); +mime.define({ + 'application/typescript': ['ts'], + 'application/json': ['code-snippets'], +}); +// From default AFD configuration +const MimeTypesToCompress = new Set([ + 'application/eot', + 'application/font', + 'application/font-sfnt', + 'application/javascript', + 'application/json', + 'application/opentype', + 'application/otf', + 'application/pkcs7-mime', + 'application/truetype', + 'application/ttf', + 'application/typescript', + 'application/vnd.ms-fontobject', + 'application/xhtml+xml', + 'application/xml', + 'application/xml+rss', + 'application/x-font-opentype', + 'application/x-font-truetype', + 'application/x-font-ttf', + 'application/x-httpd-cgi', + 'application/x-javascript', + 'application/x-mpegurl', + 'application/x-opentype', + 'application/x-otf', + 'application/x-perl', + 'application/x-ttf', + 'font/eot', + 'font/ttf', + 'font/otf', + 'font/opentype', + 'image/svg+xml', + 'text/css', + 'text/csv', + 'text/html', + 'text/javascript', + 'text/js', + 'text/markdown', + 'text/plain', + 'text/richtext', + 'text/tab-separated-values', + 'text/xml', + 'text/x-script', + 'text/x-component', + 'text/x-java-source' +]); +function wait(stream) { + return new Promise((c, e) => { + stream.on('end', () => c()); + stream.on('error', (err) => e(err)); + }); +} async function main() { const files = []; - const options = { + const options = (compressed) => ({ account: process.env.AZURE_STORAGE_ACCOUNT, credential, container: process.env.VSCODE_QUALITY, prefix: commit + '/', contentSettings: { - contentEncoding: 'gzip', + contentEncoding: compressed ? 'gzip' : undefined, cacheControl: 'max-age=31536000, public' } - }; - await new Promise((c, e) => { - vfs.src('**', { cwd: '../vscode-web', base: '../vscode-web', dot: true }) - .pipe(filter(f => !f.isDirectory())) - .pipe(gzip({ append: false })) - .pipe(es.through(function (data) { - console.log('Uploading:', data.relative); // debug - files.push(data.relative); - this.emit('data', data); - })) - .pipe(azure.upload(options)) - .on('end', () => c()) - .on('error', (err) => e(err)); }); - await new Promise((c, e) => { - const listing = new Vinyl({ - path: 'files.txt', - contents: Buffer.from(files.join('\n')), - stat: { mode: 0o666 } - }); - console.log(`Uploading: files.txt (${files.length} files)`); // debug - es.readArray([listing]) - .pipe(gzip({ append: false })) - .pipe(azure.upload(options)) - .on('end', () => c()) - .on('error', (err) => e(err)); + const all = vfs.src('**', { cwd: '../vscode-web', base: '../vscode-web', dot: true }) + .pipe(filter(f => !f.isDirectory())); + const compressed = all + .pipe(filter(f => MimeTypesToCompress.has(mime.lookup(f.path)))) + .pipe(gzip({ append: false })) + .pipe(azure.upload(options(true))); + const uncompressed = all + .pipe(filter(f => !MimeTypesToCompress.has(mime.lookup(f.path)))) + .pipe(azure.upload(options(false))); + const out = es.merge(compressed, uncompressed) + .pipe(es.through(function (f) { + console.log('Uploaded:', f.relative); + files.push(f.relative); + this.emit('data', f); + })); + console.log(`Uploading files to CDN...`); // debug + await wait(out); + const listing = new Vinyl({ + path: 'files.txt', + contents: Buffer.from(files.join('\n')), + stat: { mode: 0o666 } }); + const filesOut = es.readArray([listing]) + .pipe(gzip({ append: false })) + .pipe(azure.upload(options(true))); + console.log(`Uploading: files.txt (${files.length} files)`); // debug + await wait(filesOut); } main().catch(err => { console.error(err); process.exit(1); }); -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidXBsb2FkLWNkbi5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbInVwbG9hZC1jZG4udHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IjtBQUFBOzs7Z0dBR2dHOztBQUVoRyxtQ0FBbUM7QUFDbkMsK0JBQStCO0FBQy9CLGdDQUFnQztBQUNoQyxzQ0FBc0M7QUFDdEMsa0NBQWtDO0FBQ2xDLDhDQUF5RDtBQUN6RCxNQUFNLEtBQUssR0FBRyxPQUFPLENBQUMsb0JBQW9CLENBQUMsQ0FBQztBQUU1QyxNQUFNLE1BQU0sR0FBRyxPQUFPLENBQUMsR0FBRyxDQUFDLHNCQUFzQixDQUFDLElBQUksT0FBTyxDQUFDLEdBQUcsQ0FBQyxxQkFBcUIsQ0FBQyxDQUFDO0FBQ3pGLE1BQU0sVUFBVSxHQUFHLElBQUksaUNBQXNCLENBQUMsT0FBTyxDQUFDLEdBQUcsQ0FBQyxpQkFBaUIsQ0FBRSxFQUFFLE9BQU8sQ0FBQyxHQUFHLENBQUMsaUJBQWlCLENBQUUsRUFBRSxPQUFPLENBQUMsR0FBRyxDQUFDLHFCQUFxQixDQUFFLENBQUMsQ0FBQztBQUVySixLQUFLLFVBQVUsSUFBSTtJQUNsQixNQUFNLEtBQUssR0FBYSxFQUFFLENBQUM7SUFDM0IsTUFBTSxPQUFPLEdBQUc7UUFDZixPQUFPLEVBQUUsT0FBTyxDQUFDLEdBQUcsQ0FBQyxxQkFBcUI7UUFDMUMsVUFBVTtRQUNWLFNBQVMsRUFBRSxPQUFPLENBQUMsR0FBRyxDQUFDLGNBQWM7UUFDckMsTUFBTSxFQUFFLE1BQU0sR0FBRyxHQUFHO1FBQ3BCLGVBQWUsRUFBRTtZQUNoQixlQUFlLEVBQUUsTUFBTTtZQUN2QixZQUFZLEVBQUUsMEJBQTBCO1NBQ3hDO0tBQ0QsQ0FBQztJQUVGLE1BQU0sSUFBSSxPQUFPLENBQU8sQ0FBQyxDQUFDLEVBQUUsQ0FBQyxFQUFFLEVBQUU7UUFDaEMsR0FBRyxDQUFDLEdBQUcsQ0FBQyxJQUFJLEVBQUUsRUFBRSxHQUFHLEVBQUUsZUFBZSxFQUFFLElBQUksRUFBRSxlQUFlLEVBQUUsR0FBRyxFQUFFLElBQUksRUFBRSxDQUFDO2FBQ3ZFLElBQUksQ0FBQyxNQUFNLENBQUMsQ0FBQyxDQUFDLEVBQUUsQ0FBQyxDQUFDLENBQUMsQ0FBQyxXQUFXLEVBQUUsQ0FBQyxDQUFDO2FBQ25DLElBQUksQ0FBQyxJQUFJLENBQUMsRUFBRSxNQUFNLEVBQUUsS0FBSyxFQUFFLENBQUMsQ0FBQzthQUM3QixJQUFJLENBQUMsRUFBRSxDQUFDLE9BQU8sQ0FBQyxVQUFVLElBQVc7WUFDckMsT0FBTyxDQUFDLEdBQUcsQ0FBQyxZQUFZLEVBQUUsSUFBSSxDQUFDLFFBQVEsQ0FBQyxDQUFDLENBQUMsUUFBUTtZQUNsRCxLQUFLLENBQUMsSUFBSSxDQUFDLElBQUksQ0FBQyxRQUFRLENBQUMsQ0FBQztZQUMxQixJQUFJLENBQUMsSUFBSSxDQUFDLE1BQU0sRUFBRSxJQUFJLENBQUMsQ0FBQztRQUN6QixDQUFDLENBQUMsQ0FBQzthQUNGLElBQUksQ0FBQyxLQUFLLENBQUMsTUFBTSxDQUFDLE9BQU8sQ0FBQyxDQUFDO2FBQzNCLEVBQUUsQ0FBQyxLQUFLLEVBQUUsR0FBRyxFQUFFLENBQUMsQ0FBQyxFQUFFLENBQUM7YUFDcEIsRUFBRSxDQUFDLE9BQU8sRUFBRSxDQUFDLEdBQVEsRUFBRSxFQUFFLENBQUMsQ0FBQyxDQUFDLEdBQUcsQ0FBQyxDQUFDLENBQUM7SUFDckMsQ0FBQyxDQUFDLENBQUM7SUFFSCxNQUFNLElBQUksT0FBTyxDQUFPLENBQUMsQ0FBQyxFQUFFLENBQUMsRUFBRSxFQUFFO1FBQ2hDLE1BQU0sT0FBTyxHQUFHLElBQUksS0FBSyxDQUFDO1lBQ3pCLElBQUksRUFBRSxXQUFXO1lBQ2pCLFFBQVEsRUFBRSxNQUFNLENBQUMsSUFBSSxDQUFDLEtBQUssQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLENBQUM7WUFDdkMsSUFBSSxFQUFFLEVBQUUsSUFBSSxFQUFFLEtBQUssRUFBUztTQUM1QixDQUFDLENBQUM7UUFFSCxPQUFPLENBQUMsR0FBRyxDQUFDLHlCQUF5QixLQUFLLENBQUMsTUFBTSxTQUFTLENBQUMsQ0FBQyxDQUFDLFFBQVE7UUFDckUsRUFBRSxDQUFDLFNBQVMsQ0FBQyxDQUFDLE9BQU8sQ0FBQyxDQUFDO2FBQ3JCLElBQUksQ0FBQyxJQUFJLENBQUMsRUFBRSxNQUFNLEVBQUUsS0FBSyxFQUFFLENBQUMsQ0FBQzthQUM3QixJQUFJLENBQUMsS0FBSyxDQUFDLE1BQU0sQ0FBQyxPQUFPLENBQUMsQ0FBQzthQUMzQixFQUFFLENBQUMsS0FBSyxFQUFFLEdBQUcsRUFBRSxDQUFDLENBQUMsRUFBRSxDQUFDO2FBQ3BCLEVBQUUsQ0FBQyxPQUFPLEVBQUUsQ0FBQyxHQUFRLEVBQUUsRUFBRSxDQUFDLENBQUMsQ0FBQyxHQUFHLENBQUMsQ0FBQyxDQUFDO0lBQ3JDLENBQUMsQ0FBQyxDQUFDO0FBQ0osQ0FBQztBQUVELElBQUksRUFBRSxDQUFDLEtBQUssQ0FBQyxHQUFHLENBQUMsRUFBRTtJQUNsQixPQUFPLENBQUMsS0FBSyxDQUFDLEdBQUcsQ0FBQyxDQUFDO0lBQ25CLE9BQU8sQ0FBQyxJQUFJLENBQUMsQ0FBQyxDQUFDLENBQUM7QUFDakIsQ0FBQyxDQUFDLENBQUMifQ== \ No newline at end of file +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidXBsb2FkLWNkbi5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbInVwbG9hZC1jZG4udHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IjtBQUFBOzs7Z0dBR2dHOztBQUVoRyxtQ0FBbUM7QUFDbkMsK0JBQStCO0FBQy9CLGdDQUFnQztBQUNoQyxzQ0FBc0M7QUFDdEMsa0NBQWtDO0FBQ2xDLDZCQUE2QjtBQUM3Qiw4Q0FBeUQ7QUFDekQsTUFBTSxLQUFLLEdBQUcsT0FBTyxDQUFDLG9CQUFvQixDQUFDLENBQUM7QUFFNUMsTUFBTSxNQUFNLEdBQUcsT0FBTyxDQUFDLEdBQUcsQ0FBQyxzQkFBc0IsQ0FBQyxJQUFJLE9BQU8sQ0FBQyxHQUFHLENBQUMscUJBQXFCLENBQUMsQ0FBQztBQUN6RixNQUFNLFVBQVUsR0FBRyxJQUFJLGlDQUFzQixDQUFDLE9BQU8sQ0FBQyxHQUFHLENBQUMsaUJBQWlCLENBQUUsRUFBRSxPQUFPLENBQUMsR0FBRyxDQUFDLGlCQUFpQixDQUFFLEVBQUUsT0FBTyxDQUFDLEdBQUcsQ0FBQyxxQkFBcUIsQ0FBRSxDQUFDLENBQUM7QUFFckosSUFBSSxDQUFDLE1BQU0sQ0FBQztJQUNYLHdCQUF3QixFQUFFLENBQUMsSUFBSSxDQUFDO0lBQ2hDLGtCQUFrQixFQUFFLENBQUMsZUFBZSxDQUFDO0NBQ3JDLENBQUMsQ0FBQztBQUVILGlDQUFpQztBQUNqQyxNQUFNLG1CQUFtQixHQUFHLElBQUksR0FBRyxDQUFDO0lBQ25DLGlCQUFpQjtJQUNqQixrQkFBa0I7SUFDbEIsdUJBQXVCO0lBQ3ZCLHdCQUF3QjtJQUN4QixrQkFBa0I7SUFDbEIsc0JBQXNCO0lBQ3RCLGlCQUFpQjtJQUNqQix3QkFBd0I7SUFDeEIsc0JBQXNCO0lBQ3RCLGlCQUFpQjtJQUNqQix3QkFBd0I7SUFDeEIsK0JBQStCO0lBQy9CLHVCQUF1QjtJQUN2QixpQkFBaUI7SUFDakIscUJBQXFCO0lBQ3JCLDZCQUE2QjtJQUM3Qiw2QkFBNkI7SUFDN0Isd0JBQXdCO0lBQ3hCLHlCQUF5QjtJQUN6QiwwQkFBMEI7SUFDMUIsdUJBQXVCO0lBQ3ZCLHdCQUF3QjtJQUN4QixtQkFBbUI7SUFDbkIsb0JBQW9CO0lBQ3BCLG1CQUFtQjtJQUNuQixVQUFVO0lBQ1YsVUFBVTtJQUNWLFVBQVU7SUFDVixlQUFlO0lBQ2YsZUFBZTtJQUNmLFVBQVU7SUFDVixVQUFVO0lBQ1YsV0FBVztJQUNYLGlCQUFpQjtJQUNqQixTQUFTO0lBQ1QsZUFBZTtJQUNmLFlBQVk7SUFDWixlQUFlO0lBQ2YsMkJBQTJCO0lBQzNCLFVBQVU7SUFDVixlQUFlO0lBQ2Ysa0JBQWtCO0lBQ2xCLG9CQUFvQjtDQUNwQixDQUFDLENBQUM7QUFFSCxTQUFTLElBQUksQ0FBQyxNQUF3QjtJQUNyQyxPQUFPLElBQUksT0FBTyxDQUFPLENBQUMsQ0FBQyxFQUFFLENBQUMsRUFBRSxFQUFFO1FBQ2pDLE1BQU0sQ0FBQyxFQUFFLENBQUMsS0FBSyxFQUFFLEdBQUcsRUFBRSxDQUFDLENBQUMsRUFBRSxDQUFDLENBQUM7UUFDNUIsTUFBTSxDQUFDLEVBQUUsQ0FBQyxPQUFPLEVBQUUsQ0FBQyxHQUFRLEVBQUUsRUFBRSxDQUFDLENBQUMsQ0FBQyxHQUFHLENBQUMsQ0FBQyxDQUFDO0lBQzFDLENBQUMsQ0FBQyxDQUFDO0FBQ0osQ0FBQztBQUVELEtBQUssVUFBVSxJQUFJO0lBQ2xCLE1BQU0sS0FBSyxHQUFhLEVBQUUsQ0FBQztJQUMzQixNQUFNLE9BQU8sR0FBRyxDQUFDLFVBQW1CLEVBQUUsRUFBRSxDQUFDLENBQUM7UUFDekMsT0FBTyxFQUFFLE9BQU8sQ0FBQyxHQUFHLENBQUMscUJBQXFCO1FBQzFDLFVBQVU7UUFDVixTQUFTLEVBQUUsT0FBTyxDQUFDLEdBQUcsQ0FBQyxjQUFjO1FBQ3JDLE1BQU0sRUFBRSxNQUFNLEdBQUcsR0FBRztRQUNwQixlQUFlLEVBQUU7WUFDaEIsZUFBZSxFQUFFLFVBQVUsQ0FBQyxDQUFDLENBQUMsTUFBTSxDQUFDLENBQUMsQ0FBQyxTQUFTO1lBQ2hELFlBQVksRUFBRSwwQkFBMEI7U0FDeEM7S0FDRCxDQUFDLENBQUM7SUFFSCxNQUFNLEdBQUcsR0FBRyxHQUFHLENBQUMsR0FBRyxDQUFDLElBQUksRUFBRSxFQUFFLEdBQUcsRUFBRSxlQUFlLEVBQUUsSUFBSSxFQUFFLGVBQWUsRUFBRSxHQUFHLEVBQUUsSUFBSSxFQUFFLENBQUM7U0FDbkYsSUFBSSxDQUFDLE1BQU0sQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUFDLENBQUMsQ0FBQyxDQUFDLFdBQVcsRUFBRSxDQUFDLENBQUMsQ0FBQztJQUV0QyxNQUFNLFVBQVUsR0FBRyxHQUFHO1NBQ3BCLElBQUksQ0FBQyxNQUFNLENBQUMsQ0FBQyxDQUFDLEVBQUUsQ0FBQyxtQkFBbUIsQ0FBQyxHQUFHLENBQUMsSUFBSSxDQUFDLE1BQU0sQ0FBQyxDQUFDLENBQUMsSUFBSSxDQUFDLENBQUMsQ0FBQyxDQUFDO1NBQy9ELElBQUksQ0FBQyxJQUFJLENBQUMsRUFBRSxNQUFNLEVBQUUsS0FBSyxFQUFFLENBQUMsQ0FBQztTQUM3QixJQUFJLENBQUMsS0FBSyxDQUFDLE1BQU0sQ0FBQyxPQUFPLENBQUMsSUFBSSxDQUFDLENBQUMsQ0FBQyxDQUFDO0lBRXBDLE1BQU0sWUFBWSxHQUFHLEdBQUc7U0FDdEIsSUFBSSxDQUFDLE1BQU0sQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUFDLENBQUMsbUJBQW1CLENBQUMsR0FBRyxDQUFDLElBQUksQ0FBQyxNQUFNLENBQUMsQ0FBQyxDQUFDLElBQUksQ0FBQyxDQUFDLENBQUMsQ0FBQztTQUNoRSxJQUFJLENBQUMsS0FBSyxDQUFDLE1BQU0sQ0FBQyxPQUFPLENBQUMsS0FBSyxDQUFDLENBQUMsQ0FBQyxDQUFDO0lBRXJDLE1BQU0sR0FBRyxHQUFHLEVBQUUsQ0FBQyxLQUFLLENBQUMsVUFBVSxFQUFFLFlBQVksQ0FBQztTQUM1QyxJQUFJLENBQUMsRUFBRSxDQUFDLE9BQU8sQ0FBQyxVQUFVLENBQUM7UUFDM0IsT0FBTyxDQUFDLEdBQUcsQ0FBQyxXQUFXLEVBQUUsQ0FBQyxDQUFDLFFBQVEsQ0FBQyxDQUFDO1FBQ3JDLEtBQUssQ0FBQyxJQUFJLENBQUMsQ0FBQyxDQUFDLFFBQVEsQ0FBQyxDQUFDO1FBQ3ZCLElBQUksQ0FBQyxJQUFJLENBQUMsTUFBTSxFQUFFLENBQUMsQ0FBQyxDQUFDO0lBQ3RCLENBQUMsQ0FBQyxDQUFDLENBQUM7SUFFTCxPQUFPLENBQUMsR0FBRyxDQUFDLDJCQUEyQixDQUFDLENBQUMsQ0FBQyxRQUFRO0lBQ2xELE1BQU0sSUFBSSxDQUFDLEdBQUcsQ0FBQyxDQUFDO0lBRWhCLE1BQU0sT0FBTyxHQUFHLElBQUksS0FBSyxDQUFDO1FBQ3pCLElBQUksRUFBRSxXQUFXO1FBQ2pCLFFBQVEsRUFBRSxNQUFNLENBQUMsSUFBSSxDQUFDLEtBQUssQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLENBQUM7UUFDdkMsSUFBSSxFQUFFLEVBQUUsSUFBSSxFQUFFLEtBQUssRUFBUztLQUM1QixDQUFDLENBQUM7SUFFSCxNQUFNLFFBQVEsR0FBRyxFQUFFLENBQUMsU0FBUyxDQUFDLENBQUMsT0FBTyxDQUFDLENBQUM7U0FDdEMsSUFBSSxDQUFDLElBQUksQ0FBQyxFQUFFLE1BQU0sRUFBRSxLQUFLLEVBQUUsQ0FBQyxDQUFDO1NBQzdCLElBQUksQ0FBQyxLQUFLLENBQUMsTUFBTSxDQUFDLE9BQU8sQ0FBQyxJQUFJLENBQUMsQ0FBQyxDQUFDLENBQUM7SUFFcEMsT0FBTyxDQUFDLEdBQUcsQ0FBQyx5QkFBeUIsS0FBSyxDQUFDLE1BQU0sU0FBUyxDQUFDLENBQUMsQ0FBQyxRQUFRO0lBQ3JFLE1BQU0sSUFBSSxDQUFDLFFBQVEsQ0FBQyxDQUFDO0FBQ3RCLENBQUM7QUFFRCxJQUFJLEVBQUUsQ0FBQyxLQUFLLENBQUMsR0FBRyxDQUFDLEVBQUU7SUFDbEIsT0FBTyxDQUFDLEtBQUssQ0FBQyxHQUFHLENBQUMsQ0FBQztJQUNuQixPQUFPLENBQUMsSUFBSSxDQUFDLENBQUMsQ0FBQyxDQUFDO0FBQ2pCLENBQUMsQ0FBQyxDQUFDIn0= \ No newline at end of file diff --git a/build/azure-pipelines/upload-cdn.ts b/build/azure-pipelines/upload-cdn.ts index ddb03c1e839..753d63cb85d 100644 --- a/build/azure-pipelines/upload-cdn.ts +++ b/build/azure-pipelines/upload-cdn.ts @@ -8,53 +8,119 @@ import * as Vinyl from 'vinyl'; import * as vfs from 'vinyl-fs'; import * as filter from 'gulp-filter'; import * as gzip from 'gulp-gzip'; +import * as mime from 'mime'; import { ClientSecretCredential } from '@azure/identity'; const azure = require('gulp-azure-storage'); const commit = process.env['VSCODE_DISTRO_COMMIT'] || process.env['BUILD_SOURCEVERSION']; const credential = new ClientSecretCredential(process.env['AZURE_TENANT_ID']!, process.env['AZURE_CLIENT_ID']!, process.env['AZURE_CLIENT_SECRET']!); +mime.define({ + 'application/typescript': ['ts'], + 'application/json': ['code-snippets'], +}); + +// From default AFD configuration +const MimeTypesToCompress = new Set([ + 'application/eot', + 'application/font', + 'application/font-sfnt', + 'application/javascript', + 'application/json', + 'application/opentype', + 'application/otf', + 'application/pkcs7-mime', + 'application/truetype', + 'application/ttf', + 'application/typescript', + 'application/vnd.ms-fontobject', + 'application/xhtml+xml', + 'application/xml', + 'application/xml+rss', + 'application/x-font-opentype', + 'application/x-font-truetype', + 'application/x-font-ttf', + 'application/x-httpd-cgi', + 'application/x-javascript', + 'application/x-mpegurl', + 'application/x-opentype', + 'application/x-otf', + 'application/x-perl', + 'application/x-ttf', + 'font/eot', + 'font/ttf', + 'font/otf', + 'font/opentype', + 'image/svg+xml', + 'text/css', + 'text/csv', + 'text/html', + 'text/javascript', + 'text/js', + 'text/markdown', + 'text/plain', + 'text/richtext', + 'text/tab-separated-values', + 'text/xml', + 'text/x-script', + 'text/x-component', + 'text/x-java-source' +]); + +function wait(stream: es.ThroughStream): Promise { + return new Promise((c, e) => { + stream.on('end', () => c()); + stream.on('error', (err: any) => e(err)); + }); +} + async function main(): Promise { const files: string[] = []; - const options = { + const options = (compressed: boolean) => ({ account: process.env.AZURE_STORAGE_ACCOUNT, credential, container: process.env.VSCODE_QUALITY, prefix: commit + '/', contentSettings: { - contentEncoding: 'gzip', + contentEncoding: compressed ? 'gzip' : undefined, cacheControl: 'max-age=31536000, public' } - }; - - await new Promise((c, e) => { - vfs.src('**', { cwd: '../vscode-web', base: '../vscode-web', dot: true }) - .pipe(filter(f => !f.isDirectory())) - .pipe(gzip({ append: false })) - .pipe(es.through(function (data: Vinyl) { - console.log('Uploading:', data.relative); // debug - files.push(data.relative); - this.emit('data', data); - })) - .pipe(azure.upload(options)) - .on('end', () => c()) - .on('error', (err: any) => e(err)); }); - await new Promise((c, e) => { - const listing = new Vinyl({ - path: 'files.txt', - contents: Buffer.from(files.join('\n')), - stat: { mode: 0o666 } as any - }); + const all = vfs.src('**', { cwd: '../vscode-web', base: '../vscode-web', dot: true }) + .pipe(filter(f => !f.isDirectory())); - console.log(`Uploading: files.txt (${files.length} files)`); // debug - es.readArray([listing]) - .pipe(gzip({ append: false })) - .pipe(azure.upload(options)) - .on('end', () => c()) - .on('error', (err: any) => e(err)); + const compressed = all + .pipe(filter(f => MimeTypesToCompress.has(mime.lookup(f.path)))) + .pipe(gzip({ append: false })) + .pipe(azure.upload(options(true))); + + const uncompressed = all + .pipe(filter(f => !MimeTypesToCompress.has(mime.lookup(f.path)))) + .pipe(azure.upload(options(false))); + + const out = es.merge(compressed, uncompressed) + .pipe(es.through(function (f) { + console.log('Uploaded:', f.relative); + files.push(f.relative); + this.emit('data', f); + })); + + console.log(`Uploading files to CDN...`); // debug + await wait(out); + + const listing = new Vinyl({ + path: 'files.txt', + contents: Buffer.from(files.join('\n')), + stat: { mode: 0o666 } as any }); + + const filesOut = es.readArray([listing]) + .pipe(gzip({ append: false })) + .pipe(azure.upload(options(true))); + + console.log(`Uploading: files.txt (${files.length} files)`); // debug + await wait(filesOut); } main().catch(err => { diff --git a/build/gulpfile.vscode.js b/build/gulpfile.vscode.js index e96ab92ecac..938943d7809 100644 --- a/build/gulpfile.vscode.js +++ b/build/gulpfile.vscode.js @@ -126,7 +126,7 @@ const optimizeVSCodeTask = task.define('optimize-vscode', task.series( { src: [...windowBootstrapFiles, 'out-build/vs/code/electron-sandbox/workbench/workbench.js'], out: 'vs/code/electron-sandbox/workbench/workbench.js' }, { src: [...windowBootstrapFiles, 'out-build/vs/code/electron-sandbox/issue/issueReporter.js'], out: 'vs/code/electron-sandbox/issue/issueReporter.js' }, { src: [...windowBootstrapFiles, 'out-build/vs/code/electron-sandbox/processExplorer/processExplorer.js'], out: 'vs/code/electron-sandbox/processExplorer/processExplorer.js' }, - { src: [...windowBootstrapFiles, 'out-build/vs/code/electron-browser/sharedProcess/sharedProcess.js'], out: 'vs/code/electron-browser/sharedProcess/sharedProcess.js' } + { src: [...windowBootstrapFiles, 'out-build/vs/code/node/sharedProcess/sharedProcess.js'], out: 'vs/code/node/sharedProcess/sharedProcess.js' } ] } ) diff --git a/build/lib/i18n.resources.json b/build/lib/i18n.resources.json index 49087e64d02..afe76cf78ad 100644 --- a/build/lib/i18n.resources.json +++ b/build/lib/i18n.resources.json @@ -493,6 +493,10 @@ { "name": "vs/workbench/services/userDataProfile", "project": "vscode-profiles" + }, + { + "name": "vs/workbench/services/localization", + "project": "vscode-workbench" } ] } diff --git a/build/npm/gyp/package.json b/build/npm/gyp/package.json index 9efc7b78788..0efaa499b9d 100644 --- a/build/npm/gyp/package.json +++ b/build/npm/gyp/package.json @@ -6,6 +6,5 @@ "devDependencies": { "node-gyp": "^8.4.1" }, - "scripts": { - } + "scripts": {} } diff --git a/build/npm/gyp/yarn.lock b/build/npm/gyp/yarn.lock index ed79f4868b7..d5d6bced114 100644 --- a/build/npm/gyp/yarn.lock +++ b/build/npm/gyp/yarn.lock @@ -3,14 +3,14 @@ "@gar/promisify@^1.0.1": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.2.tgz#30aa825f11d438671d585bd44e7fd564535fc210" - integrity sha512-82cpyJyKRoQoRi+14ibCeGPu0CwypgtBAdBhq1WfvagpCZNKqwXbKwXllYSMG91DhmG4jt9gN8eP6lGOtozuaw== + version "1.1.3" + resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.3.tgz#555193ab2e3bb3b6adc3d551c9c030d9e860daf6" + integrity sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw== "@npmcli/fs@^1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@npmcli/fs/-/fs-1.0.0.tgz#589612cfad3a6ea0feafcb901d29c63fd52db09f" - integrity sha512-8ltnOpRR/oJbOp8vaGUnipOi3bqkcW+sLHFlyXIr08OGHmVJLB1Hn7QtGXbYcpVtH1gAYZTlmDXtE4YV0+AMMQ== + version "1.1.1" + resolved "https://registry.yarnpkg.com/@npmcli/fs/-/fs-1.1.1.tgz#72f719fe935e687c56a4faecf3c03d06ba593257" + integrity sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ== dependencies: "@gar/promisify" "^1.0.1" semver "^7.3.5" @@ -41,9 +41,9 @@ agent-base@6, agent-base@^6.0.2: debug "4" agentkeepalive@^4.1.3: - version "4.1.4" - resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-4.1.4.tgz#d928028a4862cb11718e55227872e842a44c945b" - integrity sha512-+V/rGa3EuU74H6wR04plBb7Ks10FbtUQgRj/FQOG7uUIEuaINI+AiqJR1k6t3SVNs7o7ZjIdus6706qqzVq8jQ== + version "4.2.1" + resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-4.2.1.tgz#a7975cbb9f83b367f06c90cc51ff28fe7d499717" + integrity sha512-Zn4cw2NEqd+9fiSVWMscnjyQ1a8Yfoc5oBajLeo5w+YBHgDUcEBY2hS4YpTz6iN5f/2zQiktcuM6tS8x1p9dpA== dependencies: debug "^4.1.0" depd "^1.1.2" @@ -67,10 +67,10 @@ ansi-regex@^5.0.1: resolved "https://registry.yarnpkg.com/aproba/-/aproba-2.0.0.tgz#52520b8ae5b569215b354efc0caa3fe1e45a8adc" integrity sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ== -are-we-there-yet@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz#372e0e7bd279d8e94c653aaa1f67200884bf3e1c" - integrity sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw== +are-we-there-yet@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz#679df222b278c64f2cdba1175cdc00b0d96164bd" + integrity sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg== dependencies: delegates "^1.0.0" readable-stream "^3.6.0" @@ -122,7 +122,7 @@ clean-stack@^2.0.0: resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== -color-support@^1.1.2: +color-support@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2" integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg== @@ -130,29 +130,29 @@ color-support@^1.1.2: concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" - integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== -console-control-strings@^1.0.0, console-control-strings@^1.1.0: +console-control-strings@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" - integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4= + integrity sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ== -debug@4, debug@^4.1.0, debug@^4.3.1: - version "4.3.3" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.3.tgz#04266e0b70a98d4462e6e288e38259213332b664" - integrity sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q== +debug@4, debug@^4.1.0, debug@^4.3.3: + version "4.3.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== dependencies: ms "2.1.2" delegates@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" - integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o= + integrity sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ== depd@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" - integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= + integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ== emoji-regex@^8.0.0: version "8.0.0" @@ -186,49 +186,48 @@ fs-minipass@^2.0.0: fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" - integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== -gauge@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/gauge/-/gauge-4.0.0.tgz#afba07aa0374a93c6219603b1fb83eaa2264d8f8" - integrity sha512-F8sU45yQpjQjxKkm1UOAhf0U/O0aFt//Fl7hsrNVto+patMHjs7dPI9mFOGUKbhrgKm0S3EjW3scMFuQmWSROw== +gauge@^4.0.3: + version "4.0.4" + resolved "https://registry.yarnpkg.com/gauge/-/gauge-4.0.4.tgz#52ff0652f2bbf607a989793d53b751bef2328dce" + integrity sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg== dependencies: - ansi-regex "^5.0.1" aproba "^1.0.3 || ^2.0.0" - color-support "^1.1.2" - console-control-strings "^1.0.0" + color-support "^1.1.3" + console-control-strings "^1.1.0" has-unicode "^2.0.1" - signal-exit "^3.0.0" + signal-exit "^3.0.7" string-width "^4.2.3" strip-ansi "^6.0.1" - wide-align "^1.1.2" + wide-align "^1.1.5" glob@^7.1.3, glob@^7.1.4: - version "7.2.0" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023" - integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q== + version "7.2.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" + integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== dependencies: fs.realpath "^1.0.0" inflight "^1.0.4" inherits "2" - minimatch "^3.0.4" + minimatch "^3.1.1" once "^1.3.0" path-is-absolute "^1.0.0" graceful-fs@^4.2.6: - version "4.2.8" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.8.tgz#e412b8d33f5e006593cbd3cee6df9f2cebbe802a" - integrity sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg== + version "4.2.10" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" + integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== has-unicode@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" - integrity sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk= + integrity sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ== http-cache-semantics@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390" - integrity sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ== + version "4.1.1" + resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz#abe02fcb2985460bf0323be664436ec3476a6d5a" + integrity sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ== http-proxy-agent@^4.0.1: version "4.0.1" @@ -240,9 +239,9 @@ http-proxy-agent@^4.0.1: debug "4" https-proxy-agent@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2" - integrity sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA== + version "5.0.1" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" + integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== dependencies: agent-base "6" debug "4" @@ -250,7 +249,7 @@ https-proxy-agent@^5.0.0: humanize-ms@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/humanize-ms/-/humanize-ms-1.2.1.tgz#c46e3159a293f6b896da29316d8b6fe8bb79bbed" - integrity sha1-xG4xWaKT9riW2ikxbYtv6Lt5u+0= + integrity sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ== dependencies: ms "^2.0.0" @@ -264,7 +263,7 @@ iconv-lite@^0.6.2: imurmurhash@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" - integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= + integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== indent-string@^4.0.0: version "4.0.0" @@ -279,7 +278,7 @@ infer-owner@^1.0.4: inflight@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" - integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= + integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== dependencies: once "^1.3.0" wrappy "1" @@ -289,10 +288,10 @@ inherits@2, inherits@^2.0.3: resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== -ip@^1.1.5: - version "1.1.5" - resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a" - integrity sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo= +ip@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ip/-/ip-2.0.0.tgz#4cf4ab182fee2314c75ede1276f8c80b479936da" + integrity sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ== is-fullwidth-code-point@^3.0.0: version "3.0.0" @@ -302,12 +301,12 @@ is-fullwidth-code-point@^3.0.0: is-lambda@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/is-lambda/-/is-lambda-1.0.1.tgz#3d9877899e6a53efc0160504cde15f82e6f061d5" - integrity sha1-PZh3iZ5qU+/AFgUEzeFfgubwYdU= + integrity sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ== isexe@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" - integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== lru-cache@^6.0.0: version "6.0.0" @@ -338,10 +337,10 @@ make-fetch-happen@^9.1.0: socks-proxy-agent "^6.0.0" ssri "^8.0.0" -minimatch@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" - integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== +minimatch@^3.1.1: + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== dependencies: brace-expansion "^1.1.7" @@ -385,12 +384,17 @@ minipass-sized@^1.0.3: minipass "^3.0.0" minipass@^3.0.0, minipass@^3.1.0, minipass@^3.1.1, minipass@^3.1.3: - version "3.1.5" - resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.1.5.tgz#71f6251b0a33a49c01b3cf97ff77eda030dff732" - integrity sha512-+8NzxD82XQoNKNrl1d/FSi+X8wAEWR+sbYAfIvub4Nz0d22plFG72CEVVaufV8PNf4qSslFTD8VMOxNVhHCjTw== + version "3.3.6" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.3.6.tgz#7bba384db3a1520d18c9c0e5251c3444e95dd94a" + integrity sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw== dependencies: yallist "^4.0.0" +minipass@^4.0.0: + version "4.0.3" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-4.0.3.tgz#00bfbaf1e16e35e804f4aa31a7c1f6b8d9f0ee72" + integrity sha512-OW2r4sQ0sI+z5ckEt5c1Tri4xTgZwYDxpE54eqWlQloQRoWtXjqt9udJ5Z4dSv7wK+nfFI7FRXyCpBSft+gpFw== + minizlib@^2.0.0, minizlib@^2.1.1: version "2.1.2" resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931" @@ -415,9 +419,9 @@ ms@^2.0.0: integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== negotiator@^0.6.2: - version "0.6.2" - resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" - integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw== + version "0.6.3" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" + integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== node-gyp@^8.4.1: version "8.4.1" @@ -443,19 +447,19 @@ nopt@^5.0.0: abbrev "1" npmlog@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-6.0.0.tgz#ba9ef39413c3d936ea91553db7be49c34ad0520c" - integrity sha512-03ppFRGlsyUaQFbGC2C8QWJN/C/K7PsfyD9aQdhVKAQIH4sQBc8WASqFBP7O+Ut4d2oo5LoeoboB3cGdBZSp6Q== + version "6.0.2" + resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-6.0.2.tgz#c8166017a42f2dea92d6453168dd865186a70830" + integrity sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg== dependencies: - are-we-there-yet "^2.0.0" + are-we-there-yet "^3.0.0" console-control-strings "^1.1.0" - gauge "^4.0.0" + gauge "^4.0.3" set-blocking "^2.0.0" once@^1.3.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" - integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== dependencies: wrappy "1" @@ -469,12 +473,12 @@ p-map@^4.0.0: path-is-absolute@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" - integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= + integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== promise-inflight@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3" - integrity sha1-mEcocL8igTL8vdhoEputEsPAKeM= + integrity sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g== promise-retry@^2.0.1: version "2.0.1" @@ -496,7 +500,7 @@ readable-stream@^3.6.0: retry@^0.12.0: version "0.12.0" resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b" - integrity sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs= + integrity sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow== rimraf@^3.0.2: version "3.0.2" @@ -516,43 +520,43 @@ safe-buffer@~5.2.0: integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== semver@^7.3.5: - version "7.3.5" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7" - integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ== + version "7.3.8" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.8.tgz#07a78feafb3f7b32347d725e33de7e2a2df67798" + integrity sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A== dependencies: lru-cache "^6.0.0" set-blocking@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" - integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= + integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw== -signal-exit@^3.0.0: - version "3.0.6" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.6.tgz#24e630c4b0f03fea446a2bd299e62b4a6ca8d0af" - integrity sha512-sDl4qMFpijcGw22U5w63KmD3cZJfBuFlVNbVMKje2keoKML7X2UzWbc4XrmEbDwg0NXJc3yv4/ox7b+JWb57kQ== +signal-exit@^3.0.7: + version "3.0.7" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" + integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== -smart-buffer@^4.1.0: +smart-buffer@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae" integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg== socks-proxy-agent@^6.0.0: - version "6.1.1" - resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-6.1.1.tgz#e664e8f1aaf4e1fb3df945f09e3d94f911137f87" - integrity sha512-t8J0kG3csjA4g6FTbsMOWws+7R7vuRC8aQ/wy3/1OWmsgwA68zs/+cExQ0koSitUDXqhufF/YJr9wtNMZHw5Ew== + version "6.2.1" + resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-6.2.1.tgz#2687a31f9d7185e38d530bef1944fe1f1496d6ce" + integrity sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ== dependencies: agent-base "^6.0.2" - debug "^4.3.1" - socks "^2.6.1" + debug "^4.3.3" + socks "^2.6.2" -socks@^2.6.1: - version "2.6.1" - resolved "https://registry.yarnpkg.com/socks/-/socks-2.6.1.tgz#989e6534a07cf337deb1b1c94aaa44296520d30e" - integrity sha512-kLQ9N5ucj8uIcxrDwjm0Jsqk06xdpBjGNQtpXy4Q8/QY2k+fY7nZH8CARy+hkbG+SGAovmzzuauCpBlb8FrnBA== +socks@^2.6.2: + version "2.7.1" + resolved "https://registry.yarnpkg.com/socks/-/socks-2.7.1.tgz#d8e651247178fde79c0663043e07240196857d55" + integrity sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ== dependencies: - ip "^1.1.5" - smart-buffer "^4.1.0" + ip "^2.0.0" + smart-buffer "^4.2.0" ssri@^8.0.0, ssri@^8.0.1: version "8.0.1" @@ -585,13 +589,13 @@ strip-ansi@^6.0.1: ansi-regex "^5.0.1" tar@^6.0.2, tar@^6.1.2: - version "6.1.11" - resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.11.tgz#6760a38f003afa1b2ffd0ffe9e9abbd0eab3d621" - integrity sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA== + version "6.1.13" + resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.13.tgz#46e22529000f612180601a6fe0680e7da508847b" + integrity sha512-jdIBIN6LTIe2jqzay/2vtYLlBHa3JF42ot3h1dW8Q0PaAG4v8rm0cvpVePtau5C6OKXGGcgO9q2AMNSWxiLqKw== dependencies: chownr "^2.0.0" fs-minipass "^2.0.0" - minipass "^3.0.0" + minipass "^4.0.0" minizlib "^2.1.1" mkdirp "^1.0.3" yallist "^4.0.0" @@ -613,7 +617,7 @@ unique-slug@^2.0.0: util-deprecate@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" - integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= + integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== which@^2.0.2: version "2.0.2" @@ -622,7 +626,7 @@ which@^2.0.2: dependencies: isexe "^2.0.0" -wide-align@^1.1.2: +wide-align@^1.1.5: version "1.1.5" resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.5.tgz#df1d4c206854369ecf3c9a4898f1b23fbd9d15d3" integrity sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg== @@ -632,7 +636,7 @@ wide-align@^1.1.2: wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" - integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== yallist@^4.0.0: version "4.0.0" diff --git a/extensions/git/package.json b/extensions/git/package.json index c7d4f94cfa2..a6ce8854c93 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -15,6 +15,7 @@ "contribEditSessions", "contribViewsWelcome", "editSessionIdentityProvider", + "quickDiffProvider", "scmActionButton", "scmSelectedProvider", "scmValidation", diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index 81e8651f846..4a12fb8eb2b 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -960,6 +960,13 @@ export class Repository implements Disposable { } } + /** + * Quick diff label + */ + get label(): string { + return l10n.t('Git local working changes'); + } + provideOriginalResource(uri: Uri): Uri | undefined { if (uri.scheme !== 'file') { return; diff --git a/extensions/microsoft-authentication/src/AADHelper.ts b/extensions/microsoft-authentication/src/AADHelper.ts index a6126dd7084..c49c708e14c 100644 --- a/extensions/microsoft-authentication/src/AADHelper.ts +++ b/extensions/microsoft-authentication/src/AADHelper.ts @@ -62,6 +62,7 @@ export interface IMicrosoftTokens { } interface IScopeData { + originalScopes?: string[]; scopes: string[]; scopeStr: string; scopesToSend: string; @@ -210,27 +211,28 @@ export class AzureActiveDirectoryService { } } + const clientId = this.getClientId(scopes); + const scopeData: IScopeData = { + clientId, + originalScopes: scopes, + scopes: modifiedScopes, + scopeStr: modifiedScopesStr, + // filter our special scopes + scopesToSend: modifiedScopes.filter(s => !s.startsWith('VSCODE_')).join(' '), + tenant: this.getTenantId(scopes), + }; + // If we still don't have a matching token try to get a new token from an existing token by using // the refreshToken. This is documented here: // https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow#refresh-the-access-token // "Refresh tokens are valid for all permissions that your client has already received consent for." if (!matchingTokens.length) { - const clientId = this.getClientId(modifiedScopes); // Get a token with the correct client id. const token = clientId === DEFAULT_CLIENT_ID ? this._tokens.find(t => t.refreshToken && !t.scope.includes('VSCODE_CLIENT_ID')) : this._tokens.find(t => t.refreshToken && t.scope.includes(`VSCODE_CLIENT_ID:${clientId}`)); if (token) { - const scopeData: IScopeData = { - clientId, - scopes: modifiedScopes, - scopeStr: modifiedScopesStr, - // filter our special scopes - scopesToSend: modifiedScopes.filter(s => !s.startsWith('VSCODE_')).join(' '), - tenant: this.getTenantId(modifiedScopes), - }; - try { const itoken = await this.refreshToken(token.refreshToken, scopeData); matchingTokens.push(itoken); @@ -241,36 +243,35 @@ export class AzureActiveDirectoryService { } Logger.info(`Got ${matchingTokens.length} sessions for scopes: ${modifiedScopesStr}`); - return Promise.all(matchingTokens.map(token => this.convertToSession(token))); + return Promise.all(matchingTokens.map(token => this.convertToSession(token, scopeData))); } public createSession(scopes: string[]): Promise { - if (!scopes.includes('openid')) { - scopes.push('openid'); + let modifiedScopes = [...scopes]; + if (!modifiedScopes.includes('openid')) { + modifiedScopes.push('openid'); } - if (!scopes.includes('email')) { - scopes.push('email'); + if (!modifiedScopes.includes('email')) { + modifiedScopes.push('email'); } - if (!scopes.includes('profile')) { - scopes.push('profile'); + if (!modifiedScopes.includes('profile')) { + modifiedScopes.push('profile'); } - if (!scopes.includes('offline_access')) { - scopes.push('offline_access'); + if (!modifiedScopes.includes('offline_access')) { + modifiedScopes.push('offline_access'); } - scopes = scopes.sort(); + modifiedScopes = modifiedScopes.sort(); const scopeData: IScopeData = { - scopes, - scopeStr: scopes.join(' '), + originalScopes: scopes, + scopes: modifiedScopes, + scopeStr: modifiedScopes.join(' '), // filter our special scopes - scopesToSend: scopes.filter(s => !s.startsWith('VSCODE_')).join(' '), + scopesToSend: modifiedScopes.filter(s => !s.startsWith('VSCODE_')).join(' '), clientId: this.getClientId(scopes), tenant: this.getTenantId(scopes), }; Logger.info(`Logging in for the following scopes: ${scopeData.scopeStr}`); - if (!scopeData.scopes.includes('offline_access')) { - Logger.info('Warning: The \'offline_access\' scope was not included, so the generated token will not be able to be refreshed.'); - } const runsRemote = vscode.env.remoteName !== undefined; const runsServerless = vscode.env.remoteName === undefined && vscode.env.uiKind === vscode.UIKind.Web; @@ -511,7 +512,7 @@ export class AzureActiveDirectoryService { }; } - private async convertToSession(token: IToken): Promise { + private async convertToSession(token: IToken, scopeData: IScopeData): Promise { if (token.accessToken && (!token.expiresAt || token.expiresAt > Date.now())) { token.expiresAt ? Logger.info(`Token available from cache (for scopes ${token.scope}), expires in ${token.expiresAt - Date.now()} milliseconds`) @@ -521,21 +522,12 @@ export class AzureActiveDirectoryService { accessToken: token.accessToken, idToken: token.idToken, account: token.account, - scopes: token.scope.split(' ') + scopes: scopeData.originalScopes ?? scopeData.scopes }; } try { Logger.info(`Token expired or unavailable (for scopes ${token.scope}), trying refresh`); - const scopes = token.scope.split(' '); - const scopeData: IScopeData = { - scopes, - scopeStr: token.scope, - // filter our special scopes - scopesToSend: scopes.filter(s => !s.startsWith('VSCODE_')).join(' '), - clientId: this.getClientId(scopes), - tenant: this.getTenantId(scopes), - }; const refreshedToken = await this.refreshToken(token.refreshToken, scopeData, token.sessionId); if (refreshedToken.accessToken) { return { @@ -543,7 +535,8 @@ export class AzureActiveDirectoryService { accessToken: refreshedToken.accessToken, idToken: refreshedToken.idToken, account: token.account, - scopes: token.scope.split(' ') + // We always prefer the original scopes requested since that array is used as a key in the AuthService + scopes: scopeData.originalScopes ?? scopeData.scopes }; } else { throw new Error(); @@ -729,7 +722,7 @@ export class AzureActiveDirectoryService { } this.setToken(token, scopeData); Logger.info(`Login successful for scopes: ${scopeData.scopeStr}`); - return await this.convertToSession(token); + return await this.convertToSession(token, scopeData); } private async fetchTokenResponse(endpoint: string, postData: string, scopeData: IScopeData): Promise { diff --git a/extensions/notebook-renderers/src/index.ts b/extensions/notebook-renderers/src/index.ts index 332faeb0226..f0f28feb2fb 100644 --- a/extensions/notebook-renderers/src/index.ts +++ b/extensions/notebook-renderers/src/index.ts @@ -31,6 +31,7 @@ interface JavaScriptRenderingHook { interface RenderOptions { readonly lineLimit: number; readonly outputScrolling: boolean; + readonly outputWordWrap: boolean; } function clearContainer(container: HTMLElement) { @@ -134,6 +135,8 @@ async function renderJavascript(outputInfo: OutputItem, container: HTMLElement, } function renderError(outputInfo: OutputItem, container: HTMLElement, ctx: RendererContext & { readonly settings: RenderOptions }): void { + clearContainer(container); + const element = document.createElement('div'); container.appendChild(element); type ErrorLike = Partial; @@ -149,6 +152,9 @@ function renderError(outputInfo: OutputItem, container: HTMLElement, ctx: Render if (err.stack) { const stack = document.createElement('pre'); stack.classList.add('traceback'); + if (ctx.settings.outputWordWrap) { + stack.classList.add('wordWrap'); + } stack.style.margin = '8px 0'; const element = document.createElement('span'); insertOutput(outputInfo.id, [err.stack ?? ''], ctx.settings.lineLimit, false, element, true); @@ -190,6 +196,11 @@ function renderStream(outputInfo: OutputItem, container: HTMLElement, error: boo const text = outputInfo.text(); const element = existing ?? document.createElement('span'); element.classList.add('output-stream'); + if (ctx.settings.outputWordWrap) { + element.classList.add('wordWrap'); + } else { + element.classList.remove('wordWrap'); + } element.setAttribute('output-item-id', outputInfo.id); insertOutput(outputInfo.id, [text], ctx.settings.lineLimit, ctx.settings.outputScrolling, element, false); outputElement.appendChild(element); @@ -199,6 +210,9 @@ function renderStream(outputInfo: OutputItem, container: HTMLElement, error: boo const element = document.createElement('span'); element.classList.add('output-stream'); + if (ctx.settings.outputWordWrap) { + element.classList.add('wordWrap'); + } element.setAttribute('output-item-id', outputInfo.id); const text = outputInfo.text(); @@ -217,6 +231,9 @@ function renderText(outputInfo: OutputItem, container: HTMLElement, ctx: Rendere clearContainer(container); const contentNode = document.createElement('div'); contentNode.classList.add('output-plaintext'); + if (ctx.settings.outputWordWrap) { + contentNode.classList.add('wordWrap'); + } const text = outputInfo.text(); insertOutput(outputInfo.id, [text], ctx.settings.lineLimit, ctx.settings.outputScrolling, contentNode, false); container.appendChild(contentNode); @@ -243,16 +260,17 @@ export const activate: ActivationFunction = (ctx) => { -webkit-user-select: text; -ms-user-select: text; cursor: auto; + word-wrap: break-word; + /* text/stream output container should scroll but preserve newline character */ + white-space: pre; } - .output-plaintext, - .output-stream { + /* When wordwrap turned on, force it to pre-wrap */ + .output-plaintext.wordWrap span, + .output-stream.wordWrap span, + .traceback.wordWrap span { white-space: pre-wrap; } - output-plaintext, - .traceback { - word-wrap: break-word; - } - .output > .scrollable { + .output .scrollable { overflow-y: scroll; max-height: var(--notebook-cell-output-max-height); border: var(--vscode-editorWidget-border); diff --git a/extensions/notebook-renderers/src/textHelper.ts b/extensions/notebook-renderers/src/textHelper.ts index 3555307ec9d..560aec613e0 100644 --- a/extensions/notebook-renderers/src/textHelper.ts +++ b/extensions/notebook-renderers/src/textHelper.ts @@ -50,14 +50,14 @@ function truncatedArrayOfString(id: string, buffer: string[], linesLimit: number } function scrollableArrayOfString(id: string, buffer: string[], container: HTMLElement, trustHtml: boolean) { - container.classList.add('scrollable'); + const scrollableDiv = document.createElement('div'); + scrollableDiv.classList.add('scrollable'); if (buffer.length > 5000) { container.appendChild(generateViewMoreElement(id, false)); } - const div = document.createElement('div'); - container.appendChild(div); - div.appendChild(handleANSIOutput(buffer.slice(0, 5000).join('\n'), trustHtml)); + container.appendChild(scrollableDiv); + scrollableDiv.appendChild(handleANSIOutput(buffer.slice(0, 5000).join('\n'), trustHtml)); } export function insertOutput(id: string, outputs: string[], linesLimit: number, scrollable: boolean, container: HTMLElement, trustHtml: boolean) { diff --git a/extensions/theme-quietlight/themes/quietlight-color-theme.json b/extensions/theme-quietlight/themes/quietlight-color-theme.json index e53088149f2..9d55f2e362b 100644 --- a/extensions/theme-quietlight/themes/quietlight-color-theme.json +++ b/extensions/theme-quietlight/themes/quietlight-color-theme.json @@ -500,7 +500,7 @@ "inputOption.activeBorder": "#adafb7", "dropdown.background": "#F5F5F5", "editor.findMatchBackground": "#BF9CAC", - "editor.findMatchHighlightBackground": "#edc9d8", + "editor.findMatchHighlightBackground": "#edc9d899", "peekViewEditor.matchHighlightBackground": "#C2DFE3", "peekViewTitle.background": "#F2F8FC", "peekViewEditor.background": "#F2F8FC", diff --git a/package.json b/package.json index 42ae1ad7505..4ef877fa5f4 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.76.0", - "distro": "ea016b36988a95ad53a905e9449f360c77fe5026", + "distro": "cd6d83a02668b3ee1577d32ede0540c6e71b8119", "author": { "name": "Microsoft Corporation" }, @@ -65,6 +65,7 @@ "@microsoft/1ds-post-js": "^3.2.2", "@parcel/watcher": "2.1.0", "@vscode/iconv-lite-umd": "0.7.0", + "@vscode/policy-watcher": "^1.1.4", "@vscode/ripgrep": "^1.14.2", "@vscode/sqlite3": "5.1.2-vscode", "@vscode/sudo-prompt": "9.3.1", @@ -83,17 +84,16 @@ "tas-client-umd": "0.1.6", "v8-inspect-profiler": "^0.1.0", "vscode-oniguruma": "1.7.0", - "vscode-policy-watcher": "^1.1.1", "vscode-proxy-agent": "^0.12.0", "vscode-regexpp": "^3.1.0", "vscode-textmate": "9.0.0", - "xterm": "5.2.0-beta.28", + "xterm": "5.2.0-beta.29", "xterm-addon-canvas": "0.4.0-beta.7", "xterm-addon-search": "0.11.0", "xterm-addon-serialize": "0.9.0", "xterm-addon-unicode11": "0.5.0", "xterm-addon-webgl": "0.15.0-beta.7", - "xterm-headless": "5.2.0-beta.28", + "xterm-headless": "5.2.0-beta.29", "yauzl": "^2.9.2", "yazl": "^2.4.3" }, diff --git a/remote/package.json b/remote/package.json index 82a66fe847d..8dbbbd73b1e 100644 --- a/remote/package.json +++ b/remote/package.json @@ -24,13 +24,13 @@ "vscode-proxy-agent": "^0.12.0", "vscode-regexpp": "^3.1.0", "vscode-textmate": "9.0.0", - "xterm": "5.2.0-beta.28", + "xterm": "5.2.0-beta.29", "xterm-addon-canvas": "0.4.0-beta.7", "xterm-addon-search": "0.11.0", "xterm-addon-serialize": "0.9.0", "xterm-addon-unicode11": "0.5.0", "xterm-addon-webgl": "0.15.0-beta.7", - "xterm-headless": "5.2.0-beta.28", + "xterm-headless": "5.2.0-beta.29", "yauzl": "^2.9.2", "yazl": "^2.4.3" }, diff --git a/remote/web/package.json b/remote/web/package.json index 0604640c76f..f2db0a6aa25 100644 --- a/remote/web/package.json +++ b/remote/web/package.json @@ -11,7 +11,7 @@ "tas-client-umd": "0.1.6", "vscode-oniguruma": "1.7.0", "vscode-textmate": "9.0.0", - "xterm": "5.2.0-beta.28", + "xterm": "5.2.0-beta.29", "xterm-addon-canvas": "0.4.0-beta.7", "xterm-addon-search": "0.11.0", "xterm-addon-unicode11": "0.5.0", diff --git a/remote/web/yarn.lock b/remote/web/yarn.lock index ae0a4d7eb1d..5d4ff870d63 100644 --- a/remote/web/yarn.lock +++ b/remote/web/yarn.lock @@ -88,7 +88,7 @@ xterm-addon-webgl@0.15.0-beta.7: resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.15.0-beta.7.tgz#ab247b499f61e8eebff92e08ec5ca999d87e06af" integrity sha512-7WCI/D6uFNp3y9TeTsbSo1h7gCy4h/yP2lWn8ZEjCaiGvO11DbKMq17fbiwaR3YmGWXoRKkcLaNIiqxFnjKO4w== -xterm@5.2.0-beta.28: - version "5.2.0-beta.28" - resolved "https://registry.yarnpkg.com/xterm/-/xterm-5.2.0-beta.28.tgz#852347e4eaf5aae7d82c90592a42adc9daab2a79" - integrity sha512-aLDxCuqjWHjvnhfWfkxy/y6coNrC+QIhbDe2sdfLPkrxhK6KnYE6qiZD5jXUIQXeq0KmSDcYi/esuKujvobC2A== +xterm@5.2.0-beta.29: + version "5.2.0-beta.29" + resolved "https://registry.yarnpkg.com/xterm/-/xterm-5.2.0-beta.29.tgz#99764aff5cd8cdb4335f5d59466b134cfcb45e3e" + integrity sha512-zx5RKcQqo78bza4R/m3WtxAJCBAF4U61fy6cxqb1PkqXF9/qdYlySUCVOauMxv+6n6cAxt3EQWwLlgvbvQBbsw== diff --git a/remote/yarn.lock b/remote/yarn.lock index 080f46a10a4..4c65e846cab 100644 --- a/remote/yarn.lock +++ b/remote/yarn.lock @@ -866,15 +866,15 @@ xterm-addon-webgl@0.15.0-beta.7: resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.15.0-beta.7.tgz#ab247b499f61e8eebff92e08ec5ca999d87e06af" integrity sha512-7WCI/D6uFNp3y9TeTsbSo1h7gCy4h/yP2lWn8ZEjCaiGvO11DbKMq17fbiwaR3YmGWXoRKkcLaNIiqxFnjKO4w== -xterm-headless@5.2.0-beta.28: - version "5.2.0-beta.28" - resolved "https://registry.yarnpkg.com/xterm-headless/-/xterm-headless-5.2.0-beta.28.tgz#d2c149da51ef138f46268b755c4fdc4202eb771c" - integrity sha512-4XcjBhFwuyjpz2ubESwp75UceySOOKdJszKyyxOQ3/7L937uiVEBBLc8T231XU8lSwWUU7czyNjYyCfpszY4+Q== +xterm-headless@5.2.0-beta.29: + version "5.2.0-beta.29" + resolved "https://registry.yarnpkg.com/xterm-headless/-/xterm-headless-5.2.0-beta.29.tgz#dd08312fdb4292c217e685d9e2e8b1957364e298" + integrity sha512-1P4urIeDTkl2C+zGb4WUnKJMACZMPGYHwVXMjkB0WhMISbkt6M34MH9ljxHhnL99dHwlx2Lvi6wvhnpyZucWCg== -xterm@5.2.0-beta.28: - version "5.2.0-beta.28" - resolved "https://registry.yarnpkg.com/xterm/-/xterm-5.2.0-beta.28.tgz#852347e4eaf5aae7d82c90592a42adc9daab2a79" - integrity sha512-aLDxCuqjWHjvnhfWfkxy/y6coNrC+QIhbDe2sdfLPkrxhK6KnYE6qiZD5jXUIQXeq0KmSDcYi/esuKujvobC2A== +xterm@5.2.0-beta.29: + version "5.2.0-beta.29" + resolved "https://registry.yarnpkg.com/xterm/-/xterm-5.2.0-beta.29.tgz#99764aff5cd8cdb4335f5d59466b134cfcb45e3e" + integrity sha512-zx5RKcQqo78bza4R/m3WtxAJCBAF4U61fy6cxqb1PkqXF9/qdYlySUCVOauMxv+6n6cAxt3EQWwLlgvbvQBbsw== yallist@^4.0.0: version "4.0.0" diff --git a/src/buildfile.js b/src/buildfile.js index de976f51000..d64321e3ffd 100644 --- a/src/buildfile.js +++ b/src/buildfile.js @@ -73,10 +73,10 @@ exports.keyboardMaps = [ exports.code = [ createModuleDescription('vs/code/electron-main/main'), - createModuleDescription('vs/code/node/cli'), - createModuleDescription('vs/code/node/cliProcessMain', ['vs/code/node/cli']), + createModuleDescription('vs/code/node/cli/main'), + createModuleDescription('vs/code/node/cli/cliProcessMain', ['vs/code/node/cli/main']), createModuleDescription('vs/code/electron-sandbox/issue/issueReporterMain'), - createModuleDescription('vs/code/electron-browser/sharedProcess/sharedProcessMain'), + createModuleDescription('vs/code/node/sharedProcess/sharedProcessMain'), createModuleDescription('vs/code/electron-sandbox/processExplorer/processExplorerMain') ]; diff --git a/src/cli.js b/src/cli.js index dd5ea5351ec..2166ce262d5 100644 --- a/src/cli.js +++ b/src/cli.js @@ -28,4 +28,4 @@ bootstrap.enableASARSupport(); process.env['VSCODE_CLI'] = '1'; // Load CLI through AMD loader -require('./bootstrap-amd').load('vs/code/node/cli'); +require('./bootstrap-amd').load('vs/code/node/cli/main'); diff --git a/src/vs/base/browser/ui/actionbar/actionViewItems.ts b/src/vs/base/browser/ui/actionbar/actionViewItems.ts index 35d21fcc416..a6e7f77a374 100644 --- a/src/vs/base/browser/ui/actionbar/actionViewItems.ts +++ b/src/vs/base/browser/ui/actionbar/actionViewItems.ts @@ -415,7 +415,7 @@ export class ActionViewItem extends BaseActionViewItem { } } -export class SelectActionViewItem extends BaseActionViewItem { +export class SelectActionViewItem extends BaseActionViewItem { protected selectBox: SelectBox; constructor(ctx: unknown, action: IAction, options: ISelectOptionItem[], selected: number, contextViewProvider: IContextViewProvider, styles: ISelectBoxStyles, selectBoxOptions?: ISelectBoxOptions) { @@ -444,7 +444,7 @@ export class SelectActionViewItem extends BaseActionViewItem { this.actionRunner.run(this._action, this.getActionContext(option, index)); } - protected getActionContext(option: string, index: number) { + protected getActionContext(option: string, index: number): T | string { return option; } diff --git a/src/vs/base/browser/ui/iconLabel/iconHoverDelegate.ts b/src/vs/base/browser/ui/iconLabel/iconHoverDelegate.ts index 1dd1e111bd9..e649c8f0c4a 100644 --- a/src/vs/base/browser/ui/iconLabel/iconHoverDelegate.ts +++ b/src/vs/base/browser/ui/iconLabel/iconHoverDelegate.ts @@ -14,11 +14,40 @@ export interface IHoverDelegateTarget extends IDisposable { } export interface IHoverDelegateOptions extends IUpdatableHoverOptions { + /** + * The content to display in the primary section of the hover. The type of text determines the + * default `hideOnHover` behavior. + */ content: IMarkdownString | string | HTMLElement; + /** + * The target for the hover. This determines the position of the hover and it will only be + * hidden when the mouse leaves both the hover and the target. A HTMLElement can be used for + * simple cases and a IHoverDelegateTarget for more complex cases where multiple elements and/or a + * dispose method is required. + */ target: IHoverDelegateTarget | HTMLElement; + /** + * Position of the hover. The default is to show above the target. This option will be ignored + * if there is not enough room to layout the hover in the specified position, unless the + * forcePosition option is set. + */ hoverPosition?: HoverPosition; + /** + * Whether to show the hover pointer + */ showPointer?: boolean; + /** + * Whether to skip the fade in animation, this should be used when hovering from one hover to + * another in the same group so it looks like the hover is moving from one element to the other. + */ skipFadeInAnimation?: boolean; + /** + * The container to pass to {@link IContextViewProvider.showContextView} which renders the hover + * in. This is particularly useful for more natural tab focusing behavior, where the hover is + * created as the next tab index after the element being hovered and/or to workaround the + * element's container hiding on `focusout`. + */ + container?: HTMLElement; } export interface IHoverDelegate { diff --git a/src/vs/base/browser/ui/iconLabel/iconLabelHover.ts b/src/vs/base/browser/ui/iconLabel/iconLabelHover.ts index 34625f28787..a36088fccc3 100644 --- a/src/vs/base/browser/ui/iconLabel/iconLabelHover.ts +++ b/src/vs/base/browser/ui/iconLabel/iconLabelHover.ts @@ -164,6 +164,7 @@ export function setupCustomHover(hoverDelegate: IHoverDelegate, htmlElement: HTM let hoverWidget: UpdatableHoverWidget | undefined; const hideHover = (disposeWidget: boolean, disposePreparation: boolean) => { + const hadHover = hoverWidget !== undefined; if (disposeWidget) { hoverWidget?.dispose(); hoverWidget = undefined; @@ -172,7 +173,9 @@ export function setupCustomHover(hoverDelegate: IHoverDelegate, htmlElement: HTM hoverPreparation?.dispose(); hoverPreparation = undefined; } - hoverDelegate.onDidHideHover?.(); + if (hadHover) { + hoverDelegate.onDidHideHover?.(); + } }; const triggerShowHover = (delay: number, focus?: boolean, target?: IHoverDelegateTarget) => { diff --git a/src/vs/base/node/ps.ts b/src/vs/base/node/ps.ts index df74c98c521..cc01a30f0ba 100644 --- a/src/vs/base/node/ps.ts +++ b/src/vs/base/node/ps.ts @@ -48,15 +48,16 @@ export function listProcesses(rootPid: number): Promise { function findName(cmd: string): string { - const SHARED_PROCESS_HINT = /--vscode-window-kind=shared-process/; - const ISSUE_REPORTER_HINT = /--vscode-window-kind=issue-reporter/; - const PROCESS_EXPLORER_HINT = /--vscode-window-kind=process-explorer/; - const UTILITY_NETWORK_HINT = /--utility-sub-type=network/; - const UTILITY_EXTENSION_HOST_HINT = /--vscode-utility-kind=extensionHost/; - const UTILITY_FILE_WATCHER_HOST_HINT = /--vscode-utility-kind=fileWatcher/; - const WINDOWS_CRASH_REPORTER = /--crashes-directory/; - const WINDOWS_PTY = /\\pipe\\winpty-control/; - const WINDOWS_CONSOLE_HOST = /conhost\.exe/; + const SHARED_PROCESS_HINT = /--vscode-window-kind=shared-process/i; // TODO@bpasero remove me + const ISSUE_REPORTER_HINT = /--vscode-window-kind=issue-reporter/i; + const PROCESS_EXPLORER_HINT = /--vscode-window-kind=process-explorer/i; + const UTILITY_NETWORK_HINT = /--utility-sub-type=network/i; + const UTILITY_EXTENSION_HOST_HINT = /--vscode-utility-kind=extensionHost/i; + const UTILITY_FILE_WATCHER_HOST_HINT = /--vscode-utility-kind=fileWatcher/i; + const UTILITY_SHARED_PROCESS_HINT = /--vscode-utility-kind=shared-process/i; + const WINDOWS_CRASH_REPORTER = /--crashes-directory/i; + const WINDOWS_PTY = /\\pipe\\winpty-control/i; + const WINDOWS_CONSOLE_HOST = /conhost\.exe/i; const TYPE = /--type=([a-zA-Z-]+)/; // find windows crash reporter @@ -103,6 +104,10 @@ export function listProcesses(rootPid: number): Promise { if (UTILITY_FILE_WATCHER_HOST_HINT.exec(cmd)) { return 'file-watcher'; } + + if (UTILITY_SHARED_PROCESS_HINT.exec(cmd)) { + return 'shared-process'; + } } else if (matches[1] === 'extensionHost') { return 'extension-host'; // normalize remote extension host type } diff --git a/src/vs/base/parts/ipc/node/ipc.mp.ts b/src/vs/base/parts/ipc/node/ipc.mp.ts index 1a1f8330251..a7cfc538d5e 100644 --- a/src/vs/base/parts/ipc/node/ipc.mp.ts +++ b/src/vs/base/parts/ipc/node/ipc.mp.ts @@ -8,6 +8,7 @@ import { VSBuffer } from 'vs/base/common/buffer'; import { ClientConnectionEvent, IMessagePassingProtocol, IPCServer } from 'vs/base/parts/ipc/common/ipc'; import { Emitter, Event } from 'vs/base/common/event'; import { assertType } from 'vs/base/common/types'; +import { firstOrDefault } from 'vs/base/common/arrays'; /** * The MessagePort `Protocol` leverages MessagePortMain style IPC communication @@ -44,8 +45,10 @@ export class Server extends IPCServer { const onCreateMessageChannel = new Emitter(); process.parentPort.on('message', (e: Electron.MessageEvent) => { - const ports = e.ports; - onCreateMessageChannel.fire(ports[0]); + const port = firstOrDefault(e.ports); + if (port) { + onCreateMessageChannel.fire(port); + } }); return Event.map(onCreateMessageChannel.event, port => { @@ -67,3 +70,19 @@ export class Server extends IPCServer { super(Server.getOnDidClientConnect()); } } + +interface INodeMessagePortFragment { + on(event: 'message', listener: (messageEvent: MessageEvent) => void): this; + removeListener(event: 'message', listener: (messageEvent: MessageEvent) => void): this; +} + +export function once(port: INodeMessagePortFragment, message: unknown, callback: () => void): void { + const listener = (e: MessageEvent) => { + if (e.data === message) { + port.removeListener('message', listener); + callback(); + } + }; + + port.on('message', listener); +} diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index c80ccdc8ea4..23c44b398cc 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -213,9 +213,8 @@ export class CodeApplication extends Disposable { }; const isAllowedWebviewRequest = (uri: URI, details: Electron.OnBeforeRequestListenerDetails): boolean => { - // Only restrict top level page of webviews: index.html if (uri.path !== '/index.html') { - return true; + return true; // Only restrict top level page of webviews: index.html } const frame = details.frame; @@ -1264,6 +1263,7 @@ export class CodeApplication extends Disposable { type: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'The type of shared process crash to understand the nature of the crash better.' }; reason: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The reason of the shared process crash to understand the nature of the crash better.' }; code: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'The exit code of the shared process crash to understand the nature of the crash better.' }; + utilityprocess: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'If the shared process is using utility process or a hidden window.' }; visible: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'Whether the shared process window was visible or not.' }; shuttingdown: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'Whether the application is shutting down when the crash happens.' }; owner: 'bpasero'; @@ -1275,6 +1275,7 @@ export class CodeApplication extends Disposable { reason: string | undefined; code: number | undefined; visible: boolean; + utilityprocess: string; shuttingdown: boolean; }; telemetryService.publicLog2('sharedprocesserror', { @@ -1282,6 +1283,7 @@ export class CodeApplication extends Disposable { reason: details?.reason, code: details?.exitCode, visible: sharedProcess.isVisible(), + utilityprocess: sharedProcess.usingUtilityProcess() ? '1' : '0', // TODO@bpasero remove this once sandbox is enabled by default shuttingdown: willShutdown }); })); diff --git a/src/vs/code/node/cliProcessMain.ts b/src/vs/code/node/cli/cliProcessMain.ts similarity index 100% rename from src/vs/code/node/cliProcessMain.ts rename to src/vs/code/node/cli/cliProcessMain.ts diff --git a/src/vs/code/node/cli.ts b/src/vs/code/node/cli/main.ts similarity index 100% rename from src/vs/code/node/cli.ts rename to src/vs/code/node/cli/main.ts diff --git a/src/vs/code/electron-browser/sharedProcess/contrib/codeCacheCleaner.ts b/src/vs/code/node/sharedProcess/contrib/codeCacheCleaner.ts similarity index 100% rename from src/vs/code/electron-browser/sharedProcess/contrib/codeCacheCleaner.ts rename to src/vs/code/node/sharedProcess/contrib/codeCacheCleaner.ts diff --git a/src/vs/code/electron-browser/sharedProcess/contrib/extensions.ts b/src/vs/code/node/sharedProcess/contrib/extensions.ts similarity index 100% rename from src/vs/code/electron-browser/sharedProcess/contrib/extensions.ts rename to src/vs/code/node/sharedProcess/contrib/extensions.ts diff --git a/src/vs/code/electron-browser/sharedProcess/contrib/languagePackCachedDataCleaner.ts b/src/vs/code/node/sharedProcess/contrib/languagePackCachedDataCleaner.ts similarity index 100% rename from src/vs/code/electron-browser/sharedProcess/contrib/languagePackCachedDataCleaner.ts rename to src/vs/code/node/sharedProcess/contrib/languagePackCachedDataCleaner.ts diff --git a/src/vs/code/electron-browser/sharedProcess/contrib/localizationsUpdater.ts b/src/vs/code/node/sharedProcess/contrib/localizationsUpdater.ts similarity index 100% rename from src/vs/code/electron-browser/sharedProcess/contrib/localizationsUpdater.ts rename to src/vs/code/node/sharedProcess/contrib/localizationsUpdater.ts diff --git a/src/vs/code/electron-browser/sharedProcess/contrib/logsDataCleaner.ts b/src/vs/code/node/sharedProcess/contrib/logsDataCleaner.ts similarity index 100% rename from src/vs/code/electron-browser/sharedProcess/contrib/logsDataCleaner.ts rename to src/vs/code/node/sharedProcess/contrib/logsDataCleaner.ts diff --git a/src/vs/code/electron-browser/sharedProcess/contrib/storageDataCleaner.ts b/src/vs/code/node/sharedProcess/contrib/storageDataCleaner.ts similarity index 96% rename from src/vs/code/electron-browser/sharedProcess/contrib/storageDataCleaner.ts rename to src/vs/code/node/sharedProcess/contrib/storageDataCleaner.ts index 1f55a52057a..bdc3d6a6b50 100644 --- a/src/vs/code/electron-browser/sharedProcess/contrib/storageDataCleaner.ts +++ b/src/vs/code/node/sharedProcess/contrib/storageDataCleaner.ts @@ -9,13 +9,16 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { join } from 'vs/base/common/path'; import { Promises } from 'vs/base/node/pfs'; import { INativeEnvironmentService } from 'vs/platform/environment/common/environment'; -import { IMainProcessService } from 'vs/platform/ipc/electron-sandbox/services'; import { ILogService } from 'vs/platform/log/common/log'; -import { INativeHostService } from 'vs/platform/native/electron-sandbox/native'; import { StorageClient } from 'vs/platform/storage/common/storageIpc'; import { EXTENSION_DEVELOPMENT_EMPTY_WINDOW_WORKSPACE } from 'vs/platform/workspace/common/workspace'; import { NON_EMPTY_WORKSPACE_ID_LENGTH } from 'vs/platform/workspaces/node/workspaces'; +/* eslint-disable local/code-layering, local/code-import-patterns */ +// TODO@bpasero layer is not allowed in utility process +import { IMainProcessService } from 'vs/platform/ipc/electron-sandbox/services'; +import { INativeHostService } from 'vs/platform/native/electron-sandbox/native'; + export class UnusedWorkspaceStorageDataCleaner extends Disposable { constructor( diff --git a/src/vs/code/electron-browser/sharedProcess/contrib/userDataProfilesCleaner.ts b/src/vs/code/node/sharedProcess/contrib/userDataProfilesCleaner.ts similarity index 100% rename from src/vs/code/electron-browser/sharedProcess/contrib/userDataProfilesCleaner.ts rename to src/vs/code/node/sharedProcess/contrib/userDataProfilesCleaner.ts diff --git a/src/vs/code/electron-browser/sharedProcess/sharedProcess-dev.html b/src/vs/code/node/sharedProcess/sharedProcess-dev.html similarity index 100% rename from src/vs/code/electron-browser/sharedProcess/sharedProcess-dev.html rename to src/vs/code/node/sharedProcess/sharedProcess-dev.html diff --git a/src/vs/code/electron-browser/sharedProcess/sharedProcess.html b/src/vs/code/node/sharedProcess/sharedProcess.html similarity index 100% rename from src/vs/code/electron-browser/sharedProcess/sharedProcess.html rename to src/vs/code/node/sharedProcess/sharedProcess.html diff --git a/src/vs/code/electron-browser/sharedProcess/sharedProcess.js b/src/vs/code/node/sharedProcess/sharedProcess.js similarity index 91% rename from src/vs/code/electron-browser/sharedProcess/sharedProcess.js rename to src/vs/code/node/sharedProcess/sharedProcess.js index 1383bc8331e..dfb0cefbede 100644 --- a/src/vs/code/electron-browser/sharedProcess/sharedProcess.js +++ b/src/vs/code/node/sharedProcess/sharedProcess.js @@ -10,7 +10,7 @@ const bootstrapWindow = bootstrapWindowLib(); // Load shared process into window - bootstrapWindow.load(['vs/code/electron-browser/sharedProcess/sharedProcessMain'], function (sharedProcess, configuration) { + bootstrapWindow.load(['vs/code/node/sharedProcess/sharedProcessMain'], function (sharedProcess, configuration) { return sharedProcess.main(configuration); }, { diff --git a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts b/src/vs/code/node/sharedProcess/sharedProcessMain.ts similarity index 86% rename from src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts rename to src/vs/code/node/sharedProcess/sharedProcessMain.ts index b158b4299b4..dd9c462b46f 100644 --- a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts +++ b/src/vs/code/node/sharedProcess/sharedProcessMain.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ipcRenderer } from 'electron'; import { hostname, release } from 'os'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import { onUnexpectedError, setUnexpectedErrorHandler } from 'vs/base/common/errors'; @@ -11,13 +10,13 @@ import { combinedDisposable, Disposable, toDisposable } from 'vs/base/common/lif import { Schemas } from 'vs/base/common/network'; import { joinPath } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; -import { ProxyChannel, StaticRouter } from 'vs/base/parts/ipc/common/ipc'; -import { Server as MessagePortServer } from 'vs/base/parts/ipc/electron-browser/ipc.mp'; -import { CodeCacheCleaner } from 'vs/code/electron-browser/sharedProcess/contrib/codeCacheCleaner'; -import { LanguagePackCachedDataCleaner } from 'vs/code/electron-browser/sharedProcess/contrib/languagePackCachedDataCleaner'; -import { LocalizationsUpdater } from 'vs/code/electron-browser/sharedProcess/contrib/localizationsUpdater'; -import { LogsDataCleaner } from 'vs/code/electron-browser/sharedProcess/contrib/logsDataCleaner'; -import { UnusedWorkspaceStorageDataCleaner } from 'vs/code/electron-browser/sharedProcess/contrib/storageDataCleaner'; +import { IPCServer, ProxyChannel, StaticRouter } from 'vs/base/parts/ipc/common/ipc'; +import { Server as UtilityProcessMessagePortServer, once } from 'vs/base/parts/ipc/node/ipc.mp'; +import { CodeCacheCleaner } from 'vs/code/node/sharedProcess/contrib/codeCacheCleaner'; +import { LanguagePackCachedDataCleaner } from 'vs/code/node/sharedProcess/contrib/languagePackCachedDataCleaner'; +import { LocalizationsUpdater } from 'vs/code/node/sharedProcess/contrib/localizationsUpdater'; +import { LogsDataCleaner } from 'vs/code/node/sharedProcess/contrib/logsDataCleaner'; +import { UnusedWorkspaceStorageDataCleaner } from 'vs/code/node/sharedProcess/contrib/storageDataCleaner'; import { IChecksumService } from 'vs/platform/checksum/common/checksumService'; import { ChecksumService } from 'vs/platform/checksum/node/checksumService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -33,10 +32,8 @@ import { ExtensionGalleryService } from 'vs/platform/extensionManagement/common/ import { IExtensionGalleryService, IExtensionManagementService, IExtensionTipsService, IGlobalExtensionEnablementService } from 'vs/platform/extensionManagement/common/extensionManagement'; import { ExtensionSignatureVerificationService, IExtensionSignatureVerificationService } from 'vs/platform/extensionManagement/node/extensionSignatureVerificationService'; import { ExtensionManagementChannel, ExtensionTipsChannel } from 'vs/platform/extensionManagement/common/extensionManagementIpc'; -import { ExtensionTipsService } from 'vs/platform/extensionManagement/electron-sandbox/extensionTipsService'; import { ExtensionManagementService, INativeServerExtensionManagementService } from 'vs/platform/extensionManagement/node/extensionManagementService'; import { IExtensionRecommendationNotificationService } from 'vs/platform/extensionRecommendations/common/extensionRecommendations'; -import { ExtensionRecommendationNotificationServiceChannelClient } from 'vs/platform/extensionRecommendations/electron-sandbox/extensionRecommendationsIpc'; import { IFileService } from 'vs/platform/files/common/files'; import { FileService } from 'vs/platform/files/common/fileService'; import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider'; @@ -44,19 +41,15 @@ import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { InstantiationService } from 'vs/platform/instantiation/common/instantiationService'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; -import { MessagePortMainProcessService } from 'vs/platform/ipc/electron-browser/mainProcessService'; -import { IMainProcessService } from 'vs/platform/ipc/electron-sandbox/services'; import { ILanguagePackService } from 'vs/platform/languagePacks/common/languagePacks'; import { NativeLanguagePackService } from 'vs/platform/languagePacks/node/languagePacks'; import { ConsoleLogger, ILoggerService, ILogService } from 'vs/platform/log/common/log'; import { LoggerChannelClient } from 'vs/platform/log/common/logIpc'; -import { INativeHostService } from 'vs/platform/native/electron-sandbox/native'; import product from 'vs/platform/product/common/product'; import { IProductService } from 'vs/platform/product/common/productService'; import { IRequestService } from 'vs/platform/request/common/request'; import { ISharedProcessConfiguration } from 'vs/platform/sharedProcess/node/sharedProcess'; import { IStorageService } from 'vs/platform/storage/common/storage'; -import { NativeStorageService } from 'vs/platform/storage/electron-sandbox/storageService'; import { resolveCommonProperties } from 'vs/platform/telemetry/common/commonProperties'; import { ICustomEndpointTelemetryService, ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { TelemetryAppenderChannel } from 'vs/platform/telemetry/common/telemetryIpc'; @@ -65,7 +58,6 @@ import { TelemetryService } from 'vs/platform/telemetry/common/telemetryService' import { supportsTelemetry, ITelemetryAppender, NullAppender, NullTelemetryService, getPiiPathsFromEnvironment, isInternalTelemetry } from 'vs/platform/telemetry/common/telemetryUtils'; import { CustomEndpointTelemetryService } from 'vs/platform/telemetry/node/customEndpointTelemetryService'; import { LocalReconnectConstants, TerminalIpcChannels, TerminalSettingId } from 'vs/platform/terminal/common/terminal'; -import { ILocalPtyService } from 'vs/platform/terminal/electron-sandbox/terminal'; import { PtyHostService } from 'vs/platform/terminal/node/ptyHostService'; import { ExtensionStorageService, IExtensionStorageService } from 'vs/platform/extensionManagement/common/extensionStorage'; import { IgnoredExtensionsManagementService, IIgnoredExtensionsManagementService } from 'vs/platform/userDataSync/common/ignoredExtensions'; @@ -79,8 +71,6 @@ import { UserDataSyncEnablementService } from 'vs/platform/userDataSync/common/u import { UserDataSyncService } from 'vs/platform/userDataSync/common/userDataSyncService'; import { UserDataSyncChannel } from 'vs/platform/userDataSync/common/userDataSyncServiceIpc'; import { UserDataSyncStoreManagementService, UserDataSyncStoreService } from 'vs/platform/userDataSync/common/userDataSyncStoreService'; -import { UserDataAutoSyncService } from 'vs/platform/userDataSync/electron-sandbox/userDataAutoSyncService'; -import { UserDataProfileStorageService } from 'vs/platform/userDataProfile/electron-sandbox/userDataProfileStorageService'; import { IUserDataProfileStorageService } from 'vs/platform/userDataProfile/common/userDataProfileStorageService'; import { ActiveWindowManager } from 'vs/platform/windows/node/windowTracker'; import { ISignService } from 'vs/platform/sign/common/sign'; @@ -90,7 +80,6 @@ import { SharedTunnelsService } from 'vs/platform/tunnel/node/tunnelService'; import { ipcSharedProcessTunnelChannelName, ISharedProcessTunnelService } from 'vs/platform/remote/common/sharedProcessTunnelService'; import { SharedProcessTunnelService } from 'vs/platform/tunnel/node/sharedProcessTunnelService'; import { ISharedProcessWorkerService } from 'vs/platform/sharedProcess/common/sharedProcessWorkerService'; -import { SharedProcessWorkerService } from 'vs/platform/sharedProcess/electron-browser/sharedProcessWorkerService'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { UriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentityService'; import { isLinux } from 'vs/base/common/platform'; @@ -104,31 +93,52 @@ import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/use import { IExtensionsProfileScannerService } from 'vs/platform/extensionManagement/common/extensionsProfileScannerService'; import { PolicyChannelClient } from 'vs/platform/policy/common/policyIpc'; import { IPolicyService, NullPolicyService } from 'vs/platform/policy/common/policy'; -import { UserDataProfilesNativeService } from 'vs/platform/userDataProfile/electron-sandbox/userDataProfile'; -import { SharedProcessRequestService } from 'vs/platform/request/electron-browser/sharedProcessRequestService'; +import { UserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfileIpc'; import { OneDataSystemAppender } from 'vs/platform/telemetry/node/1dsAppender'; -import { UserDataProfilesCleaner } from 'vs/code/electron-browser/sharedProcess/contrib/userDataProfilesCleaner'; -import { RemoteTunnelService } from 'vs/platform/remoteTunnel/electron-browser/remoteTunnelService'; +import { UserDataProfilesCleaner } from 'vs/code/node/sharedProcess/contrib/userDataProfilesCleaner'; import { IRemoteTunnelService } from 'vs/platform/remoteTunnel/common/remoteTunnel'; -import { ISharedProcessLifecycleService, SharedProcessLifecycleService } from 'vs/platform/lifecycle/electron-browser/sharedProcessLifecycleService'; import { UserDataSyncResourceProviderService } from 'vs/platform/userDataSync/common/userDataSyncResourceProvider'; -import { ExtensionsContributions } from 'vs/code/electron-browser/sharedProcess/contrib/extensions'; -import { ExtensionsProfileScannerService } from 'vs/platform/extensionManagement/electron-sandbox/extensionsProfileScannerService'; +import { ExtensionsContributions } from 'vs/code/node/sharedProcess/contrib/extensions'; import { localize } from 'vs/nls'; import { LogService } from 'vs/platform/log/common/logService'; import { ipcUtilityProcessWorkerChannelName, IUtilityProcessWorkerConfiguration } from 'vs/platform/utilityProcess/common/utilityProcessWorkerService'; +import { isUtilityProcess } from 'vs/base/parts/sandbox/node/electronTypes'; + +/* eslint-disable local/code-layering, local/code-import-patterns */ +// TODO@bpasero layer is not allowed in utility process +import { Server as BrowserWindowMessagePortServer } from 'vs/base/parts/ipc/electron-browser/ipc.mp'; +import { ExtensionTipsService } from 'vs/platform/extensionManagement/electron-sandbox/extensionTipsService'; +import { ExtensionRecommendationNotificationServiceChannelClient } from 'vs/platform/extensionRecommendations/electron-sandbox/extensionRecommendationsIpc'; +import { MessagePortMainProcessService } from 'vs/platform/ipc/electron-browser/mainProcessService'; +import { IMainProcessService } from 'vs/platform/ipc/electron-sandbox/services'; +import { INativeHostService } from 'vs/platform/native/electron-sandbox/native'; +import { NativeStorageService } from 'vs/platform/storage/electron-sandbox/storageService'; +import { ILocalPtyService } from 'vs/platform/terminal/electron-sandbox/terminal'; +import { UserDataAutoSyncService } from 'vs/platform/userDataSync/electron-sandbox/userDataAutoSyncService'; +import { UserDataProfileStorageService } from 'vs/platform/userDataProfile/electron-sandbox/userDataProfileStorageService'; +import { SharedProcessWorkerService } from 'vs/platform/sharedProcess/electron-browser/sharedProcessWorkerService'; +import { SharedProcessRequestService } from 'vs/platform/request/electron-browser/sharedProcessRequestService'; +import { RemoteTunnelService } from 'vs/platform/remoteTunnel/electron-browser/remoteTunnelService'; +import { ISharedProcessLifecycleService, SharedProcessLifecycleService } from 'vs/platform/lifecycle/electron-browser/sharedProcessLifecycleService'; +import { ExtensionsProfileScannerService } from 'vs/platform/extensionManagement/electron-sandbox/extensionsProfileScannerService'; class SharedProcessMain extends Disposable { - private server = this._register(new MessagePortServer()); + private readonly server: IPCServer; private sharedProcessWorkerService: ISharedProcessWorkerService | undefined = undefined; private lifecycleService: SharedProcessLifecycleService | undefined = undefined; - constructor(private configuration: ISharedProcessConfiguration) { + constructor(private configuration: ISharedProcessConfiguration, private ipcRenderer?: typeof import('electron').ipcRenderer) { super(); + if (isUtilityProcess(process)) { + this.server = this._register(new UtilityProcessMessagePortServer()); + } else { + this.server = this._register(new BrowserWindowMessagePortServer()); + } + this.registerListeners(); } @@ -140,25 +150,32 @@ class SharedProcessMain extends Disposable { this.dispose(); }; process.once('exit', onExit); - ipcRenderer.once('vscode:electron-main->shared-process=exit', onExit); + if (isUtilityProcess(process)) { + once(process.parentPort, 'vscode:electron-main->shared-process=exit', onExit); + } else { + this.ipcRenderer!.once('vscode:electron-main->shared-process=exit', onExit); + } - // Shared process worker lifecycle - // - // We dispose the listener when the shared process is - // disposed to avoid disposing workers when the entire - // application is shutting down anyways. - // - const eventName = 'vscode:electron-main->shared-process=disposeWorker'; - const onDisposeWorker = (event: unknown, configuration: IUtilityProcessWorkerConfiguration) => { this.onDisposeWorker(configuration); }; - ipcRenderer.on(eventName, onDisposeWorker); - this._register(toDisposable(() => ipcRenderer.removeListener(eventName, onDisposeWorker))); + if (!isUtilityProcess(process)) { + + // Shared process worker lifecycle + // + // We dispose the listener when the shared process is + // disposed to avoid disposing workers when the entire + // application is shutting down anyways. + + const eventName = 'vscode:electron-main->shared-process=disposeWorker'; + const onDisposeWorker = (event: unknown, configuration: IUtilityProcessWorkerConfiguration) => { this.onDisposeWorker(configuration); }; + this.ipcRenderer!.on(eventName, onDisposeWorker); + this._register(toDisposable(() => this.ipcRenderer!.removeListener(eventName, onDisposeWorker))); + } } private onDisposeWorker(configuration: IUtilityProcessWorkerConfiguration): void { this.sharedProcessWorkerService?.disposeWorker(configuration); } - async open(): Promise { + async init(): Promise { // Services const instantiationService = await this.initServices(); @@ -249,7 +266,7 @@ class SharedProcessMain extends Disposable { fileService.registerProvider(Schemas.vscodeUserData, userDataFileSystemProvider); // User Data Profiles - const userDataProfilesService = this._register(new UserDataProfilesNativeService(this.configuration.profiles, mainProcessService, environmentService)); + const userDataProfilesService = this._register(new UserDataProfilesService(this.configuration.profiles.all, URI.revive(this.configuration.profiles.home), mainProcessService.getChannel('userDataProfiles'))); services.set(IUserDataProfilesService, userDataProfilesService); // Configuration @@ -272,7 +289,7 @@ class SharedProcessMain extends Disposable { services.set(IUriIdentityService, uriIdentityService); // Request - services.set(IRequestService, new SharedProcessRequestService(mainProcessService, configurationService, productService, logService)); + services.set(IRequestService, new SharedProcessRequestService(mainProcessService, configurationService, logService)); // Checksum services.set(IChecksumService, new SyncDescriptor(ChecksumService, undefined, false /* proxied to other processes */)); @@ -456,15 +473,20 @@ class SharedProcessMain extends Disposable { private registerErrorHandler(logService: ILogService): void { - // Listen on unhandled rejection events - window.addEventListener('unhandledrejection', (event: PromiseRejectionEvent) => { + // Listen on global error events + if (isUtilityProcess(process)) { + process.on('uncaughtException', error => onUnexpectedError(error)); + process.on('unhandledRejection', (reason: unknown) => onUnexpectedError(reason)); + } else { + (globalThis as any).addEventListener('unhandledrejection', (event: any) => { - // See https://developer.mozilla.org/en-US/docs/Web/API/PromiseRejectionEvent - onUnexpectedError(event.reason); + // See https://developer.mozilla.org/en-US/docs/Web/API/PromiseRejectionEvent + onUnexpectedError(event.reason); - // Prevent the printing of this event to the console - event.preventDefault(); - }); + // Prevent the printing of this event to the console + event.preventDefault(); + }); + } // Install handler for unexpected errors setUnexpectedErrorHandler(error => { @@ -482,10 +504,32 @@ export async function main(configuration: ISharedProcessConfiguration): Promise< // create shared process and signal back to main that we are // ready to accept message ports as client connections - const sharedProcess = new SharedProcessMain(configuration); - ipcRenderer.send('vscode:shared-process->electron-main=ipc-ready'); + + let ipcRenderer: typeof import('electron').ipcRenderer | undefined = undefined; + if (!isUtilityProcess(process)) { + ipcRenderer = (await import('electron')).ipcRenderer; + } + + const sharedProcess = new SharedProcessMain(configuration, ipcRenderer); + + if (isUtilityProcess(process)) { + process.parentPort.postMessage('vscode:shared-process->electron-main=ipc-ready'); + } else { + ipcRenderer!.send('vscode:shared-process->electron-main=ipc-ready'); + } // await initialization and signal this back to electron-main - await sharedProcess.open(); - ipcRenderer.send('vscode:shared-process->electron-main=init-done'); + await sharedProcess.init(); + + if (isUtilityProcess(process)) { + process.parentPort.postMessage('vscode:shared-process->electron-main=init-done'); + } else { + ipcRenderer!.send('vscode:shared-process->electron-main=init-done'); + } +} + +if (isUtilityProcess(process)) { + process.parentPort.once('message', (e: Electron.MessageEvent) => { + main(e.data as ISharedProcessConfiguration); + }); } diff --git a/src/vs/editor/browser/config/editorConfiguration.ts b/src/vs/editor/browser/config/editorConfiguration.ts index e3f41e0d416..752adff3c47 100644 --- a/src/vs/editor/browser/config/editorConfiguration.ts +++ b/src/vs/editor/browser/config/editorConfiguration.ts @@ -86,6 +86,7 @@ export class EditorConfiguration extends Disposable implements IEditorConfigurat this._register(FontMeasurements.onDidChange(() => this._recomputeOptions())); this._register(browser.PixelRatio.onDidChange(() => this._recomputeOptions())); this._register(this._accessibilityService.onDidChangeScreenReaderOptimized(() => this._recomputeOptions())); + TabFocus.setTabFocusMode(this.options.get(EditorOption.tabFocusMode)); } private _recomputeOptions(): void { diff --git a/src/vs/editor/browser/editorBrowser.ts b/src/vs/editor/browser/editorBrowser.ts index f2c926f94ae..fa8ae37f759 100644 --- a/src/vs/editor/browser/editorBrowser.ts +++ b/src/vs/editor/browser/editorBrowser.ts @@ -863,7 +863,8 @@ export interface ICodeEditor extends editorCommon.IEditor { /** * All decorations added through this call will get the ownerId of this editor. - * @deprecated + * @deprecated Use `createDecorationsCollection` + * @see createDecorationsCollection */ deltaDecorations(oldDecorations: string[], newDecorations: IModelDeltaDecoration[]): string[]; diff --git a/src/vs/editor/browser/editorExtensions.ts b/src/vs/editor/browser/editorExtensions.ts index a9fb282cb57..6536ffb9034 100644 --- a/src/vs/editor/browser/editorExtensions.ts +++ b/src/vs/editor/browser/editorExtensions.ts @@ -25,10 +25,9 @@ import { IDisposable } from 'vs/base/common/lifecycle'; import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; import { ILogService } from 'vs/platform/log/common/log'; - export type ServicesAccessor = InstantiationServicesAccessor; -export type IEditorContributionCtor = IConstructorSignature; -export type IDiffEditorContributionCtor = IConstructorSignature; +export type EditorContributionCtor = IConstructorSignature; +export type DiffEditorContributionCtor = IConstructorSignature; export const enum EditorContributionInstantiation { /** @@ -65,13 +64,13 @@ export const enum EditorContributionInstantiation { export interface IEditorContributionDescription { readonly id: string; - readonly ctor: IEditorContributionCtor; + readonly ctor: EditorContributionCtor; readonly instantiation: EditorContributionInstantiation; } export interface IDiffEditorContributionDescription { id: string; - ctor: IDiffEditorContributionCtor; + ctor: DiffEditorContributionCtor; } //#region Command @@ -515,10 +514,18 @@ export function registerInstantiatedEditorAction(editorAction: EditorAction): vo EditorContributionRegistry.INSTANCE.registerEditorAction(editorAction); } +/** + * Registers an editor contribution. Editor contributions have a lifecycle which is bound + * to a specific code editor instance. + */ export function registerEditorContribution(id: string, ctor: { new(editor: ICodeEditor, ...services: Services): IEditorContribution }, instantiation: EditorContributionInstantiation): void { EditorContributionRegistry.INSTANCE.registerEditorContribution(id, ctor, instantiation); } +/** + * Registers a diff editor contribution. Diff editor contributions have a lifecycle which + * is bound to a specific diff editor instance. + */ export function registerDiffEditorContribution(id: string, ctor: { new(editor: IDiffEditor, ...services: Services): IEditorContribution }): void { EditorContributionRegistry.INSTANCE.registerDiffEditorContribution(id, ctor); } @@ -555,20 +562,16 @@ class EditorContributionRegistry { public static readonly INSTANCE = new EditorContributionRegistry(); - private readonly editorContributions: IEditorContributionDescription[]; - private readonly diffEditorContributions: IDiffEditorContributionDescription[]; - private readonly editorActions: EditorAction[]; - private readonly editorCommands: { [commandId: string]: EditorCommand }; + private readonly editorContributions: IEditorContributionDescription[] = []; + private readonly diffEditorContributions: IDiffEditorContributionDescription[] = []; + private readonly editorActions: EditorAction[] = []; + private readonly editorCommands: { [commandId: string]: EditorCommand } = Object.create(null); constructor() { - this.editorContributions = []; - this.diffEditorContributions = []; - this.editorActions = []; - this.editorCommands = Object.create(null); } public registerEditorContribution(id: string, ctor: { new(editor: ICodeEditor, ...services: Services): IEditorContribution }, instantiation: EditorContributionInstantiation): void { - this.editorContributions.push({ id, ctor: ctor as IEditorContributionCtor, instantiation }); + this.editorContributions.push({ id, ctor: ctor as EditorContributionCtor, instantiation }); } public getEditorContributions(): IEditorContributionDescription[] { @@ -576,7 +579,7 @@ class EditorContributionRegistry { } public registerDiffEditorContribution(id: string, ctor: { new(editor: IDiffEditor, ...services: Services): IEditorContribution }): void { - this.diffEditorContributions.push({ id, ctor: ctor as IDiffEditorContributionCtor }); + this.diffEditorContributions.push({ id, ctor: ctor as DiffEditorContributionCtor }); } public getDiffEditorContributions(): IDiffEditorContributionDescription[] { diff --git a/src/vs/editor/browser/services/abstractCodeEditorService.ts b/src/vs/editor/browser/services/abstractCodeEditorService.ts index fd7ffee97c5..f6268dfac42 100644 --- a/src/vs/editor/browser/services/abstractCodeEditorService.ts +++ b/src/vs/editor/browser/services/abstractCodeEditorService.ts @@ -21,12 +21,18 @@ export abstract class AbstractCodeEditorService extends Disposable implements IC declare readonly _serviceBrand: undefined; + private readonly _onWillCreateCodeEditor = this._register(new Emitter()); + public readonly onWillCreateCodeEditor = this._onWillCreateCodeEditor.event; + private readonly _onCodeEditorAdd: Emitter = this._register(new Emitter()); public readonly onCodeEditorAdd: Event = this._onCodeEditorAdd.event; private readonly _onCodeEditorRemove: Emitter = this._register(new Emitter()); public readonly onCodeEditorRemove: Event = this._onCodeEditorRemove.event; + private readonly _onWillCreateDiffEditor = this._register(new Emitter()); + public readonly onWillCreateDiffEditor = this._onWillCreateDiffEditor.event; + private readonly _onDiffEditorAdd: Emitter = this._register(new Emitter()); public readonly onDiffEditorAdd: Event = this._onDiffEditorAdd.event; @@ -55,6 +61,10 @@ export abstract class AbstractCodeEditorService extends Disposable implements IC this._globalStyleSheet = null; } + willCreateCodeEditor(): void { + this._onWillCreateCodeEditor.fire(); + } + addCodeEditor(editor: ICodeEditor): void { this._codeEditors[editor.getId()] = editor; this._onCodeEditorAdd.fire(editor); @@ -70,6 +80,10 @@ export abstract class AbstractCodeEditorService extends Disposable implements IC return Object.keys(this._codeEditors).map(id => this._codeEditors[id]); } + willCreateDiffEditor(): void { + this._onWillCreateDiffEditor.fire(); + } + addDiffEditor(editor: IDiffEditor): void { this._diffEditors[editor.getId()] = editor; this._onDiffEditorAdd.fire(editor); diff --git a/src/vs/editor/browser/services/codeEditorService.ts b/src/vs/editor/browser/services/codeEditorService.ts index 9de2e964a03..d7119b2506e 100644 --- a/src/vs/editor/browser/services/codeEditorService.ts +++ b/src/vs/editor/browser/services/codeEditorService.ts @@ -17,20 +17,23 @@ export const ICodeEditorService = createDecorator('codeEdito export interface ICodeEditorService { readonly _serviceBrand: undefined; + readonly onWillCreateCodeEditor: Event; readonly onCodeEditorAdd: Event; readonly onCodeEditorRemove: Event; + readonly onWillCreateDiffEditor: Event; readonly onDiffEditorAdd: Event; readonly onDiffEditorRemove: Event; readonly onDidChangeTransientModelProperty: Event; readonly onDecorationTypeRegistered: Event; - + willCreateCodeEditor(): void; addCodeEditor(editor: ICodeEditor): void; removeCodeEditor(editor: ICodeEditor): void; listCodeEditors(): readonly ICodeEditor[]; + willCreateDiffEditor(): void; addDiffEditor(editor: IDiffEditor): void; removeDiffEditor(editor: IDiffEditor): void; listDiffEditors(): readonly IDiffEditor[]; diff --git a/src/vs/editor/browser/widget/codeEditorWidget.ts b/src/vs/editor/browser/widget/codeEditorWidget.ts index bcfe4c68713..25d3e21cbde 100644 --- a/src/vs/editor/browser/widget/codeEditorWidget.ts +++ b/src/vs/editor/browser/widget/codeEditorWidget.ts @@ -284,6 +284,7 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE @ILanguageFeaturesService languageFeaturesService: ILanguageFeaturesService, ) { super(); + codeEditorService.willCreateCodeEditor(); const options = { ..._options }; diff --git a/src/vs/editor/browser/widget/diffEditorWidget.ts b/src/vs/editor/browser/widget/diffEditorWidget.ts index 14de272ba9f..49830dc5284 100644 --- a/src/vs/editor/browser/widget/diffEditorWidget.ts +++ b/src/vs/editor/browser/widget/diffEditorWidget.ts @@ -252,6 +252,7 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE @IEditorProgressService private readonly _editorProgressService: IEditorProgressService ) { super(); + codeEditorService.willCreateDiffEditor(); this._documentDiffProvider = this._register(instantiationService.createInstance(WorkerBasedDocumentDiffProvider, options)); this._register(this._documentDiffProvider.onDidChange(e => this._beginUpdateDecorationsSoon())); diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index 1de055a7f5c..3383600a83b 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -698,6 +698,11 @@ export interface IEditorOptions { * When enabled, this shows a preview of the drop location and triggers an `onDropIntoEditor` event. */ dropIntoEditor?: IDropIntoEditorOptions; + + /** + * Controls whether the editor receives tabs or defers them to the workbench for navigation. + */ + tabFocusMode?: boolean; } /** @@ -4569,22 +4574,6 @@ class SmartSelect extends BaseEditorOption { - - constructor() { - super(EditorOption.tabFocusMode); - } - - public compute(env: IEnvironmentalOptions, options: IComputedEditorOptions, _: boolean): boolean { - const readOnly = options.get(EditorOption.readOnly); - return (readOnly ? true : env.tabFocusMode); - } -} - -//#endregion - //#region wrappingIndent /** @@ -5619,7 +5608,9 @@ export const EditorOptions = { // Leave these at the end (because they have dependencies!) editorClassName: register(new EditorClassName()), pixelRatio: register(new EditorPixelRatio()), - tabFocusMode: register(new EditorTabFocusMode()), + tabFocusMode: register(new EditorBooleanOption(EditorOption.tabFocusMode, 'tabFocusMode', false, + { markdownDescription: nls.localize('tabFocusMode', "Controls whether the editor receives tabs or defers them to the workbench for navigation.") } + )), layoutInfo: register(new EditorLayoutInfoComputer()), wrappingInfo: register(new EditorWrappingInfoComputer()), wrappingIndent: register(new WrappingIndentOption()), diff --git a/src/vs/editor/common/editorFeatures.ts b/src/vs/editor/common/editorFeatures.ts new file mode 100644 index 00000000000..99d29778303 --- /dev/null +++ b/src/vs/editor/common/editorFeatures.ts @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { BrandedService, IConstructorSignature } from 'vs/platform/instantiation/common/instantiation'; + +/** + * A feature that will be loaded when the first code editor is constructed and disposed when the system shuts down. + */ +export interface IEditorFeature { + // Marker Interface +} + +export type EditorFeatureCtor = IConstructorSignature; + +const editorFeatures: EditorFeatureCtor[] = []; + +/** + * Registers an editor feature. Editor features will be instantiated only once, as soon as + * the first code editor is instantiated. + */ +export function registerEditorFeature(ctor: { new(...services: Services): IEditorFeature }): void { + editorFeatures.push(ctor as EditorFeatureCtor); +} + +export function getEditorFeatures(): Iterable { + return editorFeatures.slice(0); +} diff --git a/src/vs/editor/common/languages.ts b/src/vs/editor/common/languages.ts index 40bbb50e01a..ca0bb68c6bd 100644 --- a/src/vs/editor/common/languages.ts +++ b/src/vs/editor/common/languages.ts @@ -78,6 +78,9 @@ export class EncodedTokenizationResult { } } +/** + * @internal + */ export interface IBackgroundTokenizer extends IDisposable { /** * Instructs the background tokenizer to set the tokens for the given range again. diff --git a/src/vs/editor/common/model/textModelTokens.ts b/src/vs/editor/common/model/textModelTokens.ts index 7a863457afd..39ccb948fcc 100644 --- a/src/vs/editor/common/model/textModelTokens.ts +++ b/src/vs/editor/common/model/textModelTokens.ts @@ -176,8 +176,8 @@ export class TokenizationStateStore { } } - isTokenizationComplete(_textModel: ITextModel): boolean { - return this.invalidLineStartIndex >= _textModel.getLineCount(); + isTokenizationComplete(textModel: ITextModel): boolean { + return this.invalidLineStartIndex >= textModel.getLineCount(); } } @@ -264,8 +264,6 @@ export class TextModelTokenization extends Disposable { }, }; - this.backgroundTokenizer.clear(); - if (tokenizationSupport && tokenizationSupport.createBackgroundTokenizer) { this.backgroundTokenizer.value = tokenizationSupport.createBackgroundTokenizer(this._textModel, b); } @@ -273,14 +271,12 @@ export class TextModelTokenization extends Disposable { this.backgroundTokenizer.value = this._defaultBackgroundTokenizer = new DefaultBackgroundTokenizer( this._textModel, - this._tokenizationStateStore!, + this._tokenizationStateStore, b, this._languageIdCodec ); this._defaultBackgroundTokenizer.handleChanges(); } - } else { - this.backgroundTokenizer.clear(); } } diff --git a/src/vs/editor/common/model/tokenizationTextModelPart.ts b/src/vs/editor/common/model/tokenizationTextModelPart.ts index 04bcc3cdf74..603c0214087 100644 --- a/src/vs/editor/common/model/tokenizationTextModelPart.ts +++ b/src/vs/editor/common/model/tokenizationTextModelPart.ts @@ -5,7 +5,6 @@ import { Emitter, Event } from 'vs/base/common/event'; import { CharCode } from 'vs/base/common/charCode'; -import { IDisposable } from 'vs/base/common/lifecycle'; import { IPosition, Position } from 'vs/editor/common/core/position'; import { IRange, Range } from 'vs/editor/common/core/range'; import { getWordAtText, IWordAtPosition } from 'vs/editor/common/core/wordHelper'; @@ -34,7 +33,6 @@ export class TokenizationTextModelPart extends TextModelPart implements ITokeniz private readonly _onDidChangeTokens: Emitter = this._register(new Emitter()); public readonly onDidChangeTokens: Event = this._onDidChangeTokens.event; - private readonly _languageRegistryListener: IDisposable; private readonly _tokens: ContiguousTokensStore; private readonly _semanticTokens: SparseTokensStore; private readonly _tokenization: TextModelTokenization; @@ -54,25 +52,19 @@ export class TokenizationTextModelPart extends TextModelPart implements ITokeniz this._semanticTokens = new SparseTokensStore( this._languageService.languageIdCodec ); - this._tokenization = new TextModelTokenization( + this._tokenization = this._register(new TextModelTokenization( _textModel, this, this._languageService.languageIdCodec - ); + )); - this._languageRegistryListener = this._languageConfigurationService.onDidChange( + this._register(this._languageConfigurationService.onDidChange( e => { if (e.affects(this._languageId)) { this._onDidChangeLanguageConfiguration.fire({}); } } - ); - } - - public override dispose(): void { - this._languageRegistryListener.dispose(); - this._tokenization.dispose(); - super.dispose(); + )); } _hasListeners(): boolean { diff --git a/src/vs/editor/common/services/model.ts b/src/vs/editor/common/services/model.ts index 88cc4606c7e..de627268e69 100644 --- a/src/vs/editor/common/services/model.ts +++ b/src/vs/editor/common/services/model.ts @@ -9,7 +9,6 @@ import { ITextBufferFactory, ITextModel, ITextModelCreationOptions } from 'vs/ed import { ILanguageSelection } from 'vs/editor/common/languages/language'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { DocumentSemanticTokensProvider, DocumentRangeSemanticTokensProvider } from 'vs/editor/common/languages'; -import { SemanticTokensProviderStyling } from 'vs/editor/common/services/semanticTokensProviderStyling'; export const IModelService = createDecorator('modelService'); @@ -32,8 +31,6 @@ export interface IModelService { getModel(resource: URI): ITextModel | null; - getSemanticTokensProviderStyling(provider: DocumentTokensProvider): SemanticTokensProviderStyling; - onModelAdded: Event; onModelRemoved: Event; diff --git a/src/vs/editor/common/services/modelService.ts b/src/vs/editor/common/services/modelService.ts index fd8618a7de9..9a8544466d5 100644 --- a/src/vs/editor/common/services/modelService.ts +++ b/src/vs/editor/common/services/modelService.ts @@ -4,42 +4,26 @@ *--------------------------------------------------------------------------------------------*/ import { Emitter, Event } from 'vs/base/common/event'; -import { Disposable, IDisposable, DisposableStore, dispose } from 'vs/base/common/lifecycle'; +import { Disposable, IDisposable, DisposableStore } from 'vs/base/common/lifecycle'; import * as platform from 'vs/base/common/platform'; -import * as errors from 'vs/base/common/errors'; import { URI } from 'vs/base/common/uri'; import { EditOperation, ISingleEditOperation } from 'vs/editor/common/core/editOperation'; import { Range } from 'vs/editor/common/core/range'; import { DefaultEndOfLine, EndOfLinePreference, EndOfLineSequence, ITextBuffer, ITextBufferFactory, ITextModel, ITextModelCreationOptions } from 'vs/editor/common/model'; import { TextModel, createTextBuffer } from 'vs/editor/common/model/textModel'; import { EDITOR_MODEL_DEFAULTS } from 'vs/editor/common/core/textModelDefaults'; -import { IModelLanguageChangedEvent, IModelContentChangedEvent } from 'vs/editor/common/textModelEvents'; -import { DocumentSemanticTokensProvider, SemanticTokens, SemanticTokensEdits } from 'vs/editor/common/languages'; +import { IModelLanguageChangedEvent } from 'vs/editor/common/textModelEvents'; import { PLAINTEXT_LANGUAGE_ID } from 'vs/editor/common/languages/modesRegistry'; import { ILanguageSelection, ILanguageService } from 'vs/editor/common/languages/language'; -import { IModelService, DocumentTokensProvider } from 'vs/editor/common/services/model'; +import { IModelService } from 'vs/editor/common/services/model'; import { ITextResourcePropertiesService } from 'vs/editor/common/services/textResourceConfiguration'; import { IConfigurationChangeEvent, IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { RunOnceScheduler } from 'vs/base/common/async'; -import { CancellationTokenSource } from 'vs/base/common/cancellation'; -import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { ILogService } from 'vs/platform/log/common/log'; import { IUndoRedoService, ResourceEditStackSnapshot } from 'vs/platform/undoRedo/common/undoRedo'; import { StringSHA1 } from 'vs/base/common/hash'; import { isEditStackElement } from 'vs/editor/common/model/editStack'; import { Schemas } from 'vs/base/common/network'; -import { SemanticTokensProviderStyling, toMultilineTokens2 } from 'vs/editor/common/services/semanticTokensProviderStyling'; -import { getDocumentSemanticTokens, hasDocumentSemanticTokensProvider, isSemanticTokens, isSemanticTokensEdits } from 'vs/editor/common/services/getSemanticTokens'; import { equals } from 'vs/base/common/objects'; import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; -import { IFeatureDebounceInformation, ILanguageFeatureDebounceService } from 'vs/editor/common/services/languageFeatureDebounce'; -import { StopWatch } from 'vs/base/common/stopwatch'; -import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; -import { LanguageFeatureRegistry } from 'vs/editor/common/languageFeatureRegistry'; - -export interface IEditorSemanticHighlightingOptions { - enabled: true | false | 'configuredByTheme'; -} function MODEL_ID(resource: URI): string { return resource.toString(); @@ -153,30 +137,22 @@ export class ModelService extends Disposable implements IModelService { private readonly _models: { [modelId: string]: ModelData }; private readonly _disposedModels: Map; private _disposedModelsHeapSize: number; - private readonly _semanticStyling: SemanticStyling; constructor( @IConfigurationService private readonly _configurationService: IConfigurationService, @ITextResourcePropertiesService private readonly _resourcePropertiesService: ITextResourcePropertiesService, - @IThemeService private readonly _themeService: IThemeService, - @ILogService private readonly _logService: ILogService, @IUndoRedoService private readonly _undoRedoService: IUndoRedoService, @ILanguageService private readonly _languageService: ILanguageService, @ILanguageConfigurationService private readonly _languageConfigurationService: ILanguageConfigurationService, - @ILanguageFeatureDebounceService private readonly _languageFeatureDebounceService: ILanguageFeatureDebounceService, - @ILanguageFeaturesService languageFeaturesService: ILanguageFeaturesService, ) { super(); this._modelCreationOptionsByLanguageAndResource = Object.create(null); this._models = {}; this._disposedModels = new Map(); this._disposedModelsHeapSize = 0; - this._semanticStyling = this._register(new SemanticStyling(this._themeService, this._languageService, this._logService)); this._register(this._configurationService.onDidChangeConfiguration(e => this._updateModelOptions(e))); this._updateModelOptions(undefined); - - this._register(new SemanticColoringFeature(this._semanticStyling, this, this._themeService, this._configurationService, this._languageFeatureDebounceService, languageFeaturesService)); } private static _readModelOptions(config: IRawConfig, isForSimpleWidget: boolean): ITextModelCreationOptions { @@ -554,10 +530,6 @@ export class ModelService extends Disposable implements IModelService { return modelData.model; } - public getSemanticTokensProviderStyling(provider: DocumentTokensProvider): SemanticTokensProviderStyling { - return this._semanticStyling.get(provider); - } - // --- end IModelService protected _schemaShouldMaintainUndoRedoElements(resource: URI) { @@ -636,401 +608,3 @@ export class ModelService extends Disposable implements IModelService { this._onModelModeChanged.fire({ model, oldLanguageId: oldLanguageId }); } } - -export const SEMANTIC_HIGHLIGHTING_SETTING_ID = 'editor.semanticHighlighting'; - -export function isSemanticColoringEnabled(model: ITextModel, themeService: IThemeService, configurationService: IConfigurationService): boolean { - const setting = configurationService.getValue(SEMANTIC_HIGHLIGHTING_SETTING_ID, { overrideIdentifier: model.getLanguageId(), resource: model.uri })?.enabled; - if (typeof setting === 'boolean') { - return setting; - } - return themeService.getColorTheme().semanticHighlighting; -} - -class SemanticColoringFeature extends Disposable { - - private readonly _watchers: Record; - private readonly _semanticStyling: SemanticStyling; - - constructor( - semanticStyling: SemanticStyling, - @IModelService modelService: IModelService, - @IThemeService themeService: IThemeService, - @IConfigurationService configurationService: IConfigurationService, - @ILanguageFeatureDebounceService languageFeatureDebounceService: ILanguageFeatureDebounceService, - @ILanguageFeaturesService languageFeaturesService: ILanguageFeaturesService, - ) { - super(); - this._watchers = Object.create(null); - this._semanticStyling = semanticStyling; - - const register = (model: ITextModel) => { - this._watchers[model.uri.toString()] = new ModelSemanticColoring(model, this._semanticStyling, themeService, languageFeatureDebounceService, languageFeaturesService); - }; - const deregister = (model: ITextModel, modelSemanticColoring: ModelSemanticColoring) => { - modelSemanticColoring.dispose(); - delete this._watchers[model.uri.toString()]; - }; - const handleSettingOrThemeChange = () => { - for (const model of modelService.getModels()) { - const curr = this._watchers[model.uri.toString()]; - if (isSemanticColoringEnabled(model, themeService, configurationService)) { - if (!curr) { - register(model); - } - } else { - if (curr) { - deregister(model, curr); - } - } - } - }; - this._register(modelService.onModelAdded((model) => { - if (isSemanticColoringEnabled(model, themeService, configurationService)) { - register(model); - } - })); - this._register(modelService.onModelRemoved((model) => { - const curr = this._watchers[model.uri.toString()]; - if (curr) { - deregister(model, curr); - } - })); - this._register(configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration(SEMANTIC_HIGHLIGHTING_SETTING_ID)) { - handleSettingOrThemeChange(); - } - })); - this._register(themeService.onDidColorThemeChange(handleSettingOrThemeChange)); - } - - override dispose(): void { - // Dispose all watchers - for (const watcher of Object.values(this._watchers)) { - watcher.dispose(); - } - super.dispose(); - } -} - -class SemanticStyling extends Disposable { - - private _caches: WeakMap; - - constructor( - private readonly _themeService: IThemeService, - private readonly _languageService: ILanguageService, - private readonly _logService: ILogService - ) { - super(); - this._caches = new WeakMap(); - this._register(this._themeService.onDidColorThemeChange(() => { - this._caches = new WeakMap(); - })); - } - - public get(provider: DocumentTokensProvider): SemanticTokensProviderStyling { - if (!this._caches.has(provider)) { - this._caches.set(provider, new SemanticTokensProviderStyling(provider.getLegend(), this._themeService, this._languageService, this._logService)); - } - return this._caches.get(provider)!; - } -} - -class SemanticTokensResponse { - constructor( - public readonly provider: DocumentSemanticTokensProvider, - public readonly resultId: string | undefined, - public readonly data: Uint32Array - ) { } - - public dispose(): void { - this.provider.releaseDocumentSemanticTokens(this.resultId); - } -} - -class ModelSemanticColoring extends Disposable { - - public static REQUEST_MIN_DELAY = 300; - public static REQUEST_MAX_DELAY = 2000; - - private _isDisposed: boolean; - private readonly _model: ITextModel; - private readonly _semanticStyling: SemanticStyling; - private readonly _provider: LanguageFeatureRegistry; - private readonly _debounceInformation: IFeatureDebounceInformation; - private readonly _fetchDocumentSemanticTokens: RunOnceScheduler; - private _currentDocumentResponse: SemanticTokensResponse | null; - private _currentDocumentRequestCancellationTokenSource: CancellationTokenSource | null; - private _documentProvidersChangeListeners: IDisposable[]; - private _providersChangedDuringRequest: boolean; - - constructor( - model: ITextModel, - stylingProvider: SemanticStyling, - @IThemeService themeService: IThemeService, - @ILanguageFeatureDebounceService languageFeatureDebounceService: ILanguageFeatureDebounceService, - @ILanguageFeaturesService languageFeaturesService: ILanguageFeaturesService, - ) { - super(); - - this._isDisposed = false; - this._model = model; - this._semanticStyling = stylingProvider; - this._provider = languageFeaturesService.documentSemanticTokensProvider; - this._debounceInformation = languageFeatureDebounceService.for(this._provider, 'DocumentSemanticTokens', { min: ModelSemanticColoring.REQUEST_MIN_DELAY, max: ModelSemanticColoring.REQUEST_MAX_DELAY }); - this._fetchDocumentSemanticTokens = this._register(new RunOnceScheduler(() => this._fetchDocumentSemanticTokensNow(), ModelSemanticColoring.REQUEST_MIN_DELAY)); - this._currentDocumentResponse = null; - this._currentDocumentRequestCancellationTokenSource = null; - this._documentProvidersChangeListeners = []; - this._providersChangedDuringRequest = false; - - this._register(this._model.onDidChangeContent(() => { - if (!this._fetchDocumentSemanticTokens.isScheduled()) { - this._fetchDocumentSemanticTokens.schedule(this._debounceInformation.get(this._model)); - } - })); - this._register(this._model.onDidChangeLanguage(() => { - // clear any outstanding state - if (this._currentDocumentResponse) { - this._currentDocumentResponse.dispose(); - this._currentDocumentResponse = null; - } - if (this._currentDocumentRequestCancellationTokenSource) { - this._currentDocumentRequestCancellationTokenSource.cancel(); - this._currentDocumentRequestCancellationTokenSource = null; - } - this._setDocumentSemanticTokens(null, null, null, []); - this._fetchDocumentSemanticTokens.schedule(0); - })); - - const bindDocumentChangeListeners = () => { - dispose(this._documentProvidersChangeListeners); - this._documentProvidersChangeListeners = []; - for (const provider of this._provider.all(model)) { - if (typeof provider.onDidChange === 'function') { - this._documentProvidersChangeListeners.push(provider.onDidChange(() => { - if (this._currentDocumentRequestCancellationTokenSource) { - // there is already a request running, - this._providersChangedDuringRequest = true; - return; - } - this._fetchDocumentSemanticTokens.schedule(0); - })); - } - } - }; - bindDocumentChangeListeners(); - this._register(this._provider.onDidChange(() => { - bindDocumentChangeListeners(); - this._fetchDocumentSemanticTokens.schedule(this._debounceInformation.get(this._model)); - })); - - this._register(themeService.onDidColorThemeChange(_ => { - // clear out existing tokens - this._setDocumentSemanticTokens(null, null, null, []); - this._fetchDocumentSemanticTokens.schedule(this._debounceInformation.get(this._model)); - })); - - this._fetchDocumentSemanticTokens.schedule(0); - } - - public override dispose(): void { - if (this._currentDocumentResponse) { - this._currentDocumentResponse.dispose(); - this._currentDocumentResponse = null; - } - if (this._currentDocumentRequestCancellationTokenSource) { - this._currentDocumentRequestCancellationTokenSource.cancel(); - this._currentDocumentRequestCancellationTokenSource = null; - } - this._setDocumentSemanticTokens(null, null, null, []); - this._isDisposed = true; - - super.dispose(); - } - - private _fetchDocumentSemanticTokensNow(): void { - if (this._currentDocumentRequestCancellationTokenSource) { - // there is already a request running, let it finish... - return; - } - - if (!hasDocumentSemanticTokensProvider(this._provider, this._model)) { - // there is no provider - if (this._currentDocumentResponse) { - // there are semantic tokens set - this._model.tokenization.setSemanticTokens(null, false); - } - return; - } - - const cancellationTokenSource = new CancellationTokenSource(); - const lastProvider = this._currentDocumentResponse ? this._currentDocumentResponse.provider : null; - const lastResultId = this._currentDocumentResponse ? this._currentDocumentResponse.resultId || null : null; - const request = getDocumentSemanticTokens(this._provider, this._model, lastProvider, lastResultId, cancellationTokenSource.token); - this._currentDocumentRequestCancellationTokenSource = cancellationTokenSource; - this._providersChangedDuringRequest = false; - - const pendingChanges: IModelContentChangedEvent[] = []; - const contentChangeListener = this._model.onDidChangeContent((e) => { - pendingChanges.push(e); - }); - - const sw = new StopWatch(false); - request.then((res) => { - this._debounceInformation.update(this._model, sw.elapsed()); - this._currentDocumentRequestCancellationTokenSource = null; - contentChangeListener.dispose(); - - if (!res) { - this._setDocumentSemanticTokens(null, null, null, pendingChanges); - } else { - const { provider, tokens } = res; - const styling = this._semanticStyling.get(provider); - this._setDocumentSemanticTokens(provider, tokens || null, styling, pendingChanges); - } - }, (err) => { - const isExpectedError = err && (errors.isCancellationError(err) || (typeof err.message === 'string' && err.message.indexOf('busy') !== -1)); - if (!isExpectedError) { - errors.onUnexpectedError(err); - } - - // Semantic tokens eats up all errors and considers errors to mean that the result is temporarily not available - // The API does not have a special error kind to express this... - this._currentDocumentRequestCancellationTokenSource = null; - contentChangeListener.dispose(); - - if (pendingChanges.length > 0 || this._providersChangedDuringRequest) { - // More changes occurred while the request was running - if (!this._fetchDocumentSemanticTokens.isScheduled()) { - this._fetchDocumentSemanticTokens.schedule(this._debounceInformation.get(this._model)); - } - } - }); - } - - private static _copy(src: Uint32Array, srcOffset: number, dest: Uint32Array, destOffset: number, length: number): void { - // protect against overflows - length = Math.min(length, dest.length - destOffset, src.length - srcOffset); - for (let i = 0; i < length; i++) { - dest[destOffset + i] = src[srcOffset + i]; - } - } - - private _setDocumentSemanticTokens(provider: DocumentSemanticTokensProvider | null, tokens: SemanticTokens | SemanticTokensEdits | null, styling: SemanticTokensProviderStyling | null, pendingChanges: IModelContentChangedEvent[]): void { - const currentResponse = this._currentDocumentResponse; - const rescheduleIfNeeded = () => { - if ((pendingChanges.length > 0 || this._providersChangedDuringRequest) && !this._fetchDocumentSemanticTokens.isScheduled()) { - this._fetchDocumentSemanticTokens.schedule(this._debounceInformation.get(this._model)); - } - }; - - if (this._currentDocumentResponse) { - this._currentDocumentResponse.dispose(); - this._currentDocumentResponse = null; - } - if (this._isDisposed) { - // disposed! - if (provider && tokens) { - provider.releaseDocumentSemanticTokens(tokens.resultId); - } - return; - } - if (!provider || !styling) { - this._model.tokenization.setSemanticTokens(null, false); - return; - } - if (!tokens) { - this._model.tokenization.setSemanticTokens(null, true); - rescheduleIfNeeded(); - return; - } - - if (isSemanticTokensEdits(tokens)) { - if (!currentResponse) { - // not possible! - this._model.tokenization.setSemanticTokens(null, true); - return; - } - if (tokens.edits.length === 0) { - // nothing to do! - tokens = { - resultId: tokens.resultId, - data: currentResponse.data - }; - } else { - let deltaLength = 0; - for (const edit of tokens.edits) { - deltaLength += (edit.data ? edit.data.length : 0) - edit.deleteCount; - } - - const srcData = currentResponse.data; - const destData = new Uint32Array(srcData.length + deltaLength); - - let srcLastStart = srcData.length; - let destLastStart = destData.length; - for (let i = tokens.edits.length - 1; i >= 0; i--) { - const edit = tokens.edits[i]; - - if (edit.start > srcData.length) { - styling.warnInvalidEditStart(currentResponse.resultId, tokens.resultId, i, edit.start, srcData.length); - // The edits are invalid and there's no way to recover - this._model.tokenization.setSemanticTokens(null, true); - return; - } - - const copyCount = srcLastStart - (edit.start + edit.deleteCount); - if (copyCount > 0) { - ModelSemanticColoring._copy(srcData, srcLastStart - copyCount, destData, destLastStart - copyCount, copyCount); - destLastStart -= copyCount; - } - - if (edit.data) { - ModelSemanticColoring._copy(edit.data, 0, destData, destLastStart - edit.data.length, edit.data.length); - destLastStart -= edit.data.length; - } - - srcLastStart = edit.start; - } - - if (srcLastStart > 0) { - ModelSemanticColoring._copy(srcData, 0, destData, 0, srcLastStart); - } - - tokens = { - resultId: tokens.resultId, - data: destData - }; - } - } - - if (isSemanticTokens(tokens)) { - - this._currentDocumentResponse = new SemanticTokensResponse(provider, tokens.resultId, tokens.data); - - const result = toMultilineTokens2(tokens, styling, this._model.getLanguageId()); - - // Adjust incoming semantic tokens - if (pendingChanges.length > 0) { - // More changes occurred while the request was running - // We need to: - // 1. Adjust incoming semantic tokens - // 2. Request them again - for (const change of pendingChanges) { - for (const area of result) { - for (const singleChange of change.changes) { - area.applyEdit(singleChange.range, singleChange.text); - } - } - } - } - - this._model.tokenization.setSemanticTokens(result, true); - } else { - this._model.tokenization.setSemanticTokens(null, true); - } - - rescheduleIfNeeded(); - } -} diff --git a/src/vs/editor/common/services/semanticTokensStyling.ts b/src/vs/editor/common/services/semanticTokensStyling.ts new file mode 100644 index 00000000000..935c9aed528 --- /dev/null +++ b/src/vs/editor/common/services/semanticTokensStyling.ts @@ -0,0 +1,18 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { DocumentSemanticTokensProvider, DocumentRangeSemanticTokensProvider } from 'vs/editor/common/languages'; +import { SemanticTokensProviderStyling } from 'vs/editor/common/services/semanticTokensProviderStyling'; + +export const ISemanticTokensStylingService = createDecorator('semanticTokensStylingService'); + +export type DocumentTokensProvider = DocumentSemanticTokensProvider | DocumentRangeSemanticTokensProvider; + +export interface ISemanticTokensStylingService { + readonly _serviceBrand: undefined; + + getStyling(provider: DocumentTokensProvider): SemanticTokensProviderStyling; +} diff --git a/src/vs/editor/common/services/semanticTokensStylingService.ts b/src/vs/editor/common/services/semanticTokensStylingService.ts new file mode 100644 index 00000000000..8aba1f0a601 --- /dev/null +++ b/src/vs/editor/common/services/semanticTokensStylingService.ts @@ -0,0 +1,41 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable } from 'vs/base/common/lifecycle'; +import { ILanguageService } from 'vs/editor/common/languages/language'; +import { DocumentTokensProvider } from 'vs/editor/common/services/model'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { ILogService } from 'vs/platform/log/common/log'; +import { SemanticTokensProviderStyling } from 'vs/editor/common/services/semanticTokensProviderStyling'; +import { ISemanticTokensStylingService } from 'vs/editor/common/services/semanticTokensStyling'; +import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; + +export class SemanticTokensStylingService extends Disposable implements ISemanticTokensStylingService { + + public _serviceBrand: undefined; + + private _caches: WeakMap; + + constructor( + @IThemeService private readonly _themeService: IThemeService, + @ILogService private readonly _logService: ILogService, + @ILanguageService private readonly _languageService: ILanguageService, + ) { + super(); + this._caches = new WeakMap(); + this._register(this._themeService.onDidColorThemeChange(() => { + this._caches = new WeakMap(); + })); + } + + public getStyling(provider: DocumentTokensProvider): SemanticTokensProviderStyling { + if (!this._caches.has(provider)) { + this._caches.set(provider, new SemanticTokensProviderStyling(provider.getLegend(), this._themeService, this._languageService, this._logService)); + } + return this._caches.get(provider)!; + } +} + +registerSingleton(ISemanticTokensStylingService, SemanticTokensStylingService, InstantiationType.Delayed); diff --git a/src/vs/editor/contrib/format/browser/formatActions.ts b/src/vs/editor/contrib/format/browser/formatActions.ts index 934d5f4a6cc..2abca269100 100644 --- a/src/vs/editor/contrib/format/browser/formatActions.ts +++ b/src/vs/editor/contrib/format/browser/formatActions.ts @@ -40,7 +40,6 @@ class FormatOnType implements IEditorContribution { @ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService, @IEditorWorkerService private readonly _workerService: IEditorWorkerService ) { - this._disposables.add(_languageFeaturesService.onTypeFormattingEditProvider.onDidChange(this._update, this)); this._disposables.add(_editor.onDidChangeModel(() => this._update())); this._disposables.add(_editor.onDidChangeModelLanguage(() => this._update())); @@ -49,6 +48,7 @@ class FormatOnType implements IEditorContribution { this._update(); } })); + this._update(); } dispose(): void { diff --git a/src/vs/editor/contrib/peekView/browser/peekView.ts b/src/vs/editor/contrib/peekView/browser/peekView.ts index 08427a42eca..49df433bf1a 100644 --- a/src/vs/editor/contrib/peekView/browser/peekView.ts +++ b/src/vs/editor/contrib/peekView/browser/peekView.ts @@ -112,6 +112,7 @@ export abstract class PeekViewWidget extends ZoneWidget { private disposed?: true; protected _headElement?: HTMLDivElement; + protected _titleElement?: HTMLDivElement; protected _primaryHeading?: HTMLElement; protected _secondaryHeading?: HTMLElement; protected _metaHeading?: HTMLElement; @@ -180,18 +181,18 @@ export abstract class PeekViewWidget extends ZoneWidget { } protected _fillHead(container: HTMLElement, noCloseAction?: boolean): void { - const titleElement = dom.$('.peekview-title'); + this._titleElement = dom.$('.peekview-title'); if ((this.options as IPeekViewOptions).supportOnTitleClick) { - titleElement.classList.add('clickable'); - dom.addStandardDisposableListener(titleElement, 'click', event => this._onTitleClick(event)); + this._titleElement.classList.add('clickable'); + dom.addStandardDisposableListener(this._titleElement, 'click', event => this._onTitleClick(event)); } - dom.append(this._headElement!, titleElement); + dom.append(this._headElement!, this._titleElement); - this._fillTitleIcon(titleElement); + this._fillTitleIcon(this._titleElement); this._primaryHeading = dom.$('span.filename'); this._secondaryHeading = dom.$('span.dirname'); this._metaHeading = dom.$('span.meta'); - dom.append(titleElement, this._primaryHeading, this._secondaryHeading, this._metaHeading); + dom.append(this._titleElement, this._primaryHeading, this._secondaryHeading, this._metaHeading); const actionsContainer = dom.$('.peekview-actions'); dom.append(this._headElement!, actionsContainer); diff --git a/src/vs/editor/contrib/semanticTokens/browser/documentSemanticTokens.ts b/src/vs/editor/contrib/semanticTokens/browser/documentSemanticTokens.ts new file mode 100644 index 00000000000..43c004886d0 --- /dev/null +++ b/src/vs/editor/contrib/semanticTokens/browser/documentSemanticTokens.ts @@ -0,0 +1,386 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle'; +import * as errors from 'vs/base/common/errors'; +import { ITextModel } from 'vs/editor/common/model'; +import { IModelContentChangedEvent } from 'vs/editor/common/textModelEvents'; +import { DocumentSemanticTokensProvider, SemanticTokens, SemanticTokensEdits } from 'vs/editor/common/languages'; +import { IModelService } from 'vs/editor/common/services/model'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { RunOnceScheduler } from 'vs/base/common/async'; +import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { SemanticTokensProviderStyling, toMultilineTokens2 } from 'vs/editor/common/services/semanticTokensProviderStyling'; +import { getDocumentSemanticTokens, hasDocumentSemanticTokensProvider, isSemanticTokens, isSemanticTokensEdits } from 'vs/editor/contrib/semanticTokens/common/getSemanticTokens'; +import { IFeatureDebounceInformation, ILanguageFeatureDebounceService } from 'vs/editor/common/services/languageFeatureDebounce'; +import { StopWatch } from 'vs/base/common/stopwatch'; +import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; +import { LanguageFeatureRegistry } from 'vs/editor/common/languageFeatureRegistry'; +import { ISemanticTokensStylingService } from 'vs/editor/common/services/semanticTokensStyling'; +import { registerEditorFeature } from 'vs/editor/common/editorFeatures'; +import { SEMANTIC_HIGHLIGHTING_SETTING_ID, isSemanticColoringEnabled } from 'vs/editor/contrib/semanticTokens/common/semanticTokensConfig'; + +export class DocumentSemanticTokensFeature extends Disposable { + + private readonly _watchers: Record; + + constructor( + @ISemanticTokensStylingService semanticTokensStylingService: ISemanticTokensStylingService, + @IModelService modelService: IModelService, + @IThemeService themeService: IThemeService, + @IConfigurationService configurationService: IConfigurationService, + @ILanguageFeatureDebounceService languageFeatureDebounceService: ILanguageFeatureDebounceService, + @ILanguageFeaturesService languageFeaturesService: ILanguageFeaturesService, + ) { + super(); + this._watchers = Object.create(null); + + const register = (model: ITextModel) => { + this._watchers[model.uri.toString()] = new ModelSemanticColoring(model, semanticTokensStylingService, themeService, languageFeatureDebounceService, languageFeaturesService); + }; + const deregister = (model: ITextModel, modelSemanticColoring: ModelSemanticColoring) => { + modelSemanticColoring.dispose(); + delete this._watchers[model.uri.toString()]; + }; + const handleSettingOrThemeChange = () => { + for (const model of modelService.getModels()) { + const curr = this._watchers[model.uri.toString()]; + if (isSemanticColoringEnabled(model, themeService, configurationService)) { + if (!curr) { + register(model); + } + } else { + if (curr) { + deregister(model, curr); + } + } + } + }; + this._register(modelService.onModelAdded((model) => { + if (isSemanticColoringEnabled(model, themeService, configurationService)) { + register(model); + } + })); + this._register(modelService.onModelRemoved((model) => { + const curr = this._watchers[model.uri.toString()]; + if (curr) { + deregister(model, curr); + } + })); + this._register(configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(SEMANTIC_HIGHLIGHTING_SETTING_ID)) { + handleSettingOrThemeChange(); + } + })); + this._register(themeService.onDidColorThemeChange(handleSettingOrThemeChange)); + } + + override dispose(): void { + // Dispose all watchers + for (const watcher of Object.values(this._watchers)) { + watcher.dispose(); + } + super.dispose(); + } +} + +class ModelSemanticColoring extends Disposable { + + public static REQUEST_MIN_DELAY = 300; + public static REQUEST_MAX_DELAY = 2000; + + private _isDisposed: boolean; + private readonly _model: ITextModel; + private readonly _provider: LanguageFeatureRegistry; + private readonly _debounceInformation: IFeatureDebounceInformation; + private readonly _fetchDocumentSemanticTokens: RunOnceScheduler; + private _currentDocumentResponse: SemanticTokensResponse | null; + private _currentDocumentRequestCancellationTokenSource: CancellationTokenSource | null; + private _documentProvidersChangeListeners: IDisposable[]; + private _providersChangedDuringRequest: boolean; + + constructor( + model: ITextModel, + @ISemanticTokensStylingService private readonly _semanticTokensStylingService: ISemanticTokensStylingService, + @IThemeService themeService: IThemeService, + @ILanguageFeatureDebounceService languageFeatureDebounceService: ILanguageFeatureDebounceService, + @ILanguageFeaturesService languageFeaturesService: ILanguageFeaturesService, + ) { + super(); + + this._isDisposed = false; + this._model = model; + this._provider = languageFeaturesService.documentSemanticTokensProvider; + this._debounceInformation = languageFeatureDebounceService.for(this._provider, 'DocumentSemanticTokens', { min: ModelSemanticColoring.REQUEST_MIN_DELAY, max: ModelSemanticColoring.REQUEST_MAX_DELAY }); + this._fetchDocumentSemanticTokens = this._register(new RunOnceScheduler(() => this._fetchDocumentSemanticTokensNow(), ModelSemanticColoring.REQUEST_MIN_DELAY)); + this._currentDocumentResponse = null; + this._currentDocumentRequestCancellationTokenSource = null; + this._documentProvidersChangeListeners = []; + this._providersChangedDuringRequest = false; + + this._register(this._model.onDidChangeContent(() => { + if (!this._fetchDocumentSemanticTokens.isScheduled()) { + this._fetchDocumentSemanticTokens.schedule(this._debounceInformation.get(this._model)); + } + })); + this._register(this._model.onDidChangeLanguage(() => { + // clear any outstanding state + if (this._currentDocumentResponse) { + this._currentDocumentResponse.dispose(); + this._currentDocumentResponse = null; + } + if (this._currentDocumentRequestCancellationTokenSource) { + this._currentDocumentRequestCancellationTokenSource.cancel(); + this._currentDocumentRequestCancellationTokenSource = null; + } + this._setDocumentSemanticTokens(null, null, null, []); + this._fetchDocumentSemanticTokens.schedule(0); + })); + + const bindDocumentChangeListeners = () => { + dispose(this._documentProvidersChangeListeners); + this._documentProvidersChangeListeners = []; + for (const provider of this._provider.all(model)) { + if (typeof provider.onDidChange === 'function') { + this._documentProvidersChangeListeners.push(provider.onDidChange(() => { + if (this._currentDocumentRequestCancellationTokenSource) { + // there is already a request running, + this._providersChangedDuringRequest = true; + return; + } + this._fetchDocumentSemanticTokens.schedule(0); + })); + } + } + }; + bindDocumentChangeListeners(); + this._register(this._provider.onDidChange(() => { + bindDocumentChangeListeners(); + this._fetchDocumentSemanticTokens.schedule(this._debounceInformation.get(this._model)); + })); + + this._register(themeService.onDidColorThemeChange(_ => { + // clear out existing tokens + this._setDocumentSemanticTokens(null, null, null, []); + this._fetchDocumentSemanticTokens.schedule(this._debounceInformation.get(this._model)); + })); + + this._fetchDocumentSemanticTokens.schedule(0); + } + + public override dispose(): void { + if (this._currentDocumentResponse) { + this._currentDocumentResponse.dispose(); + this._currentDocumentResponse = null; + } + if (this._currentDocumentRequestCancellationTokenSource) { + this._currentDocumentRequestCancellationTokenSource.cancel(); + this._currentDocumentRequestCancellationTokenSource = null; + } + this._setDocumentSemanticTokens(null, null, null, []); + this._isDisposed = true; + + super.dispose(); + } + + private _fetchDocumentSemanticTokensNow(): void { + if (this._currentDocumentRequestCancellationTokenSource) { + // there is already a request running, let it finish... + return; + } + + if (!hasDocumentSemanticTokensProvider(this._provider, this._model)) { + // there is no provider + if (this._currentDocumentResponse) { + // there are semantic tokens set + this._model.tokenization.setSemanticTokens(null, false); + } + return; + } + + const cancellationTokenSource = new CancellationTokenSource(); + const lastProvider = this._currentDocumentResponse ? this._currentDocumentResponse.provider : null; + const lastResultId = this._currentDocumentResponse ? this._currentDocumentResponse.resultId || null : null; + const request = getDocumentSemanticTokens(this._provider, this._model, lastProvider, lastResultId, cancellationTokenSource.token); + this._currentDocumentRequestCancellationTokenSource = cancellationTokenSource; + this._providersChangedDuringRequest = false; + + const pendingChanges: IModelContentChangedEvent[] = []; + const contentChangeListener = this._model.onDidChangeContent((e) => { + pendingChanges.push(e); + }); + + const sw = new StopWatch(false); + request.then((res) => { + this._debounceInformation.update(this._model, sw.elapsed()); + this._currentDocumentRequestCancellationTokenSource = null; + contentChangeListener.dispose(); + + if (!res) { + this._setDocumentSemanticTokens(null, null, null, pendingChanges); + } else { + const { provider, tokens } = res; + const styling = this._semanticTokensStylingService.getStyling(provider); + this._setDocumentSemanticTokens(provider, tokens || null, styling, pendingChanges); + } + }, (err) => { + const isExpectedError = err && (errors.isCancellationError(err) || (typeof err.message === 'string' && err.message.indexOf('busy') !== -1)); + if (!isExpectedError) { + errors.onUnexpectedError(err); + } + + // Semantic tokens eats up all errors and considers errors to mean that the result is temporarily not available + // The API does not have a special error kind to express this... + this._currentDocumentRequestCancellationTokenSource = null; + contentChangeListener.dispose(); + + if (pendingChanges.length > 0 || this._providersChangedDuringRequest) { + // More changes occurred while the request was running + if (!this._fetchDocumentSemanticTokens.isScheduled()) { + this._fetchDocumentSemanticTokens.schedule(this._debounceInformation.get(this._model)); + } + } + }); + } + + private static _copy(src: Uint32Array, srcOffset: number, dest: Uint32Array, destOffset: number, length: number): void { + // protect against overflows + length = Math.min(length, dest.length - destOffset, src.length - srcOffset); + for (let i = 0; i < length; i++) { + dest[destOffset + i] = src[srcOffset + i]; + } + } + + private _setDocumentSemanticTokens(provider: DocumentSemanticTokensProvider | null, tokens: SemanticTokens | SemanticTokensEdits | null, styling: SemanticTokensProviderStyling | null, pendingChanges: IModelContentChangedEvent[]): void { + const currentResponse = this._currentDocumentResponse; + const rescheduleIfNeeded = () => { + if ((pendingChanges.length > 0 || this._providersChangedDuringRequest) && !this._fetchDocumentSemanticTokens.isScheduled()) { + this._fetchDocumentSemanticTokens.schedule(this._debounceInformation.get(this._model)); + } + }; + + if (this._currentDocumentResponse) { + this._currentDocumentResponse.dispose(); + this._currentDocumentResponse = null; + } + if (this._isDisposed) { + // disposed! + if (provider && tokens) { + provider.releaseDocumentSemanticTokens(tokens.resultId); + } + return; + } + if (!provider || !styling) { + this._model.tokenization.setSemanticTokens(null, false); + return; + } + if (!tokens) { + this._model.tokenization.setSemanticTokens(null, true); + rescheduleIfNeeded(); + return; + } + + if (isSemanticTokensEdits(tokens)) { + if (!currentResponse) { + // not possible! + this._model.tokenization.setSemanticTokens(null, true); + return; + } + if (tokens.edits.length === 0) { + // nothing to do! + tokens = { + resultId: tokens.resultId, + data: currentResponse.data + }; + } else { + let deltaLength = 0; + for (const edit of tokens.edits) { + deltaLength += (edit.data ? edit.data.length : 0) - edit.deleteCount; + } + + const srcData = currentResponse.data; + const destData = new Uint32Array(srcData.length + deltaLength); + + let srcLastStart = srcData.length; + let destLastStart = destData.length; + for (let i = tokens.edits.length - 1; i >= 0; i--) { + const edit = tokens.edits[i]; + + if (edit.start > srcData.length) { + styling.warnInvalidEditStart(currentResponse.resultId, tokens.resultId, i, edit.start, srcData.length); + // The edits are invalid and there's no way to recover + this._model.tokenization.setSemanticTokens(null, true); + return; + } + + const copyCount = srcLastStart - (edit.start + edit.deleteCount); + if (copyCount > 0) { + ModelSemanticColoring._copy(srcData, srcLastStart - copyCount, destData, destLastStart - copyCount, copyCount); + destLastStart -= copyCount; + } + + if (edit.data) { + ModelSemanticColoring._copy(edit.data, 0, destData, destLastStart - edit.data.length, edit.data.length); + destLastStart -= edit.data.length; + } + + srcLastStart = edit.start; + } + + if (srcLastStart > 0) { + ModelSemanticColoring._copy(srcData, 0, destData, 0, srcLastStart); + } + + tokens = { + resultId: tokens.resultId, + data: destData + }; + } + } + + if (isSemanticTokens(tokens)) { + + this._currentDocumentResponse = new SemanticTokensResponse(provider, tokens.resultId, tokens.data); + + const result = toMultilineTokens2(tokens, styling, this._model.getLanguageId()); + + // Adjust incoming semantic tokens + if (pendingChanges.length > 0) { + // More changes occurred while the request was running + // We need to: + // 1. Adjust incoming semantic tokens + // 2. Request them again + for (const change of pendingChanges) { + for (const area of result) { + for (const singleChange of change.changes) { + area.applyEdit(singleChange.range, singleChange.text); + } + } + } + } + + this._model.tokenization.setSemanticTokens(result, true); + } else { + this._model.tokenization.setSemanticTokens(null, true); + } + + rescheduleIfNeeded(); + } +} + +class SemanticTokensResponse { + constructor( + public readonly provider: DocumentSemanticTokensProvider, + public readonly resultId: string | undefined, + public readonly data: Uint32Array + ) { } + + public dispose(): void { + this.provider.releaseDocumentSemanticTokens(this.resultId); + } +} + +registerEditorFeature(DocumentSemanticTokensFeature); diff --git a/src/vs/editor/contrib/viewportSemanticTokens/browser/viewportSemanticTokens.ts b/src/vs/editor/contrib/semanticTokens/browser/viewportSemanticTokens.ts similarity index 93% rename from src/vs/editor/contrib/viewportSemanticTokens/browser/viewportSemanticTokens.ts rename to src/vs/editor/contrib/semanticTokens/browser/viewportSemanticTokens.ts index 98842559f70..ccf205ae1b6 100644 --- a/src/vs/editor/contrib/viewportSemanticTokens/browser/viewportSemanticTokens.ts +++ b/src/vs/editor/contrib/semanticTokens/browser/viewportSemanticTokens.ts @@ -10,9 +10,8 @@ import { EditorContributionInstantiation, registerEditorContribution } from 'vs/ import { Range } from 'vs/editor/common/core/range'; import { IEditorContribution } from 'vs/editor/common/editorCommon'; import { ITextModel } from 'vs/editor/common/model'; -import { getDocumentRangeSemanticTokens, hasDocumentRangeSemanticTokensProvider } from 'vs/editor/common/services/getSemanticTokens'; -import { IModelService } from 'vs/editor/common/services/model'; -import { isSemanticColoringEnabled, SEMANTIC_HIGHLIGHTING_SETTING_ID } from 'vs/editor/common/services/modelService'; +import { getDocumentRangeSemanticTokens, hasDocumentRangeSemanticTokensProvider } from 'vs/editor/contrib/semanticTokens/common/getSemanticTokens'; +import { isSemanticColoringEnabled, SEMANTIC_HIGHLIGHTING_SETTING_ID } from 'vs/editor/contrib/semanticTokens/common/semanticTokensConfig'; import { toMultilineTokens2 } from 'vs/editor/common/services/semanticTokensProviderStyling'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IThemeService } from 'vs/platform/theme/common/themeService'; @@ -21,6 +20,7 @@ import { StopWatch } from 'vs/base/common/stopwatch'; import { LanguageFeatureRegistry } from 'vs/editor/common/languageFeatureRegistry'; import { DocumentRangeSemanticTokensProvider } from 'vs/editor/common/languages'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; +import { ISemanticTokensStylingService } from 'vs/editor/common/services/semanticTokensStyling'; class ViewportSemanticTokensContribution extends Disposable implements IEditorContribution { @@ -38,7 +38,7 @@ class ViewportSemanticTokensContribution extends Disposable implements IEditorCo constructor( editor: ICodeEditor, - @IModelService private readonly _modelService: IModelService, + @ISemanticTokensStylingService private readonly _semanticTokensStylingService: ISemanticTokensStylingService, @IThemeService private readonly _themeService: IThemeService, @IConfigurationService private readonly _configurationService: IConfigurationService, @ILanguageFeatureDebounceService languageFeatureDebounceService: ILanguageFeatureDebounceService, @@ -134,7 +134,7 @@ class ViewportSemanticTokensContribution extends Disposable implements IEditorCo return; } const { provider, tokens: result } = r; - const styling = this._modelService.getSemanticTokensProviderStyling(provider); + const styling = this._semanticTokensStylingService.getStyling(provider); model.tokenization.setPartialSemanticTokens(range, toMultilineTokens2(result, styling, model.getLanguageId())); }).then(() => this._removeOutstandingRequest(request), () => this._removeOutstandingRequest(request)); return request; diff --git a/src/vs/editor/common/services/getSemanticTokens.ts b/src/vs/editor/contrib/semanticTokens/common/getSemanticTokens.ts similarity index 100% rename from src/vs/editor/common/services/getSemanticTokens.ts rename to src/vs/editor/contrib/semanticTokens/common/getSemanticTokens.ts diff --git a/src/vs/editor/contrib/semanticTokens/common/semanticTokensConfig.ts b/src/vs/editor/contrib/semanticTokens/common/semanticTokensConfig.ts new file mode 100644 index 00000000000..ebf4f3b2932 --- /dev/null +++ b/src/vs/editor/contrib/semanticTokens/common/semanticTokensConfig.ts @@ -0,0 +1,22 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ITextModel } from 'vs/editor/common/model'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; + +export const SEMANTIC_HIGHLIGHTING_SETTING_ID = 'editor.semanticHighlighting'; + +export interface IEditorSemanticHighlightingOptions { + enabled: true | false | 'configuredByTheme'; +} + +export function isSemanticColoringEnabled(model: ITextModel, themeService: IThemeService, configurationService: IConfigurationService): boolean { + const setting = configurationService.getValue(SEMANTIC_HIGHLIGHTING_SETTING_ID, { overrideIdentifier: model.getLanguageId(), resource: model.uri })?.enabled; + if (typeof setting === 'boolean') { + return setting; + } + return themeService.getColorTheme().semanticHighlighting; +} diff --git a/src/vs/editor/contrib/semanticTokens/test/browser/documentSemanticTokens.test.ts b/src/vs/editor/contrib/semanticTokens/test/browser/documentSemanticTokens.test.ts new file mode 100644 index 00000000000..9a4c4876426 --- /dev/null +++ b/src/vs/editor/contrib/semanticTokens/test/browser/documentSemanticTokens.test.ts @@ -0,0 +1,278 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { Emitter, Event } from 'vs/base/common/event'; +import { Range } from 'vs/editor/common/core/range'; +import { ITextModel } from 'vs/editor/common/model'; +import { ModelService } from 'vs/editor/common/services/modelService'; +import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; +import { TestColorTheme, TestThemeService } from 'vs/platform/theme/test/common/testThemeService'; +import { NullLogService } from 'vs/platform/log/common/log'; +import { UndoRedoService } from 'vs/platform/undoRedo/common/undoRedoService'; +import { TestDialogService } from 'vs/platform/dialogs/test/common/testDialogService'; +import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { DocumentSemanticTokensProvider, SemanticTokens, SemanticTokensEdits, SemanticTokensLegend } from 'vs/editor/common/languages'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { Barrier, timeout } from 'vs/base/common/async'; +import { LanguageService } from 'vs/editor/common/services/languageService'; +import { ColorScheme } from 'vs/platform/theme/common/theme'; +import { IModelService } from 'vs/editor/common/services/model'; +import { ILanguageService } from 'vs/editor/common/languages/language'; +import { TestTextResourcePropertiesService } from 'vs/editor/test/common/services/testTextResourcePropertiesService'; +import { TestLanguageConfigurationService } from 'vs/editor/test/common/modes/testLanguageConfigurationService'; +import { getDocumentSemanticTokens, isSemanticTokens } from 'vs/editor/contrib/semanticTokens/common/getSemanticTokens'; +import { LanguageFeatureDebounceService } from 'vs/editor/common/services/languageFeatureDebounce'; +import { runWithFakedTimers } from 'vs/base/test/common/timeTravelScheduler'; +import { LanguageFeaturesService } from 'vs/editor/common/services/languageFeaturesService'; +import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; +import { SemanticTokensStylingService } from 'vs/editor/common/services/semanticTokensStylingService'; +import { DocumentSemanticTokensFeature } from 'vs/editor/contrib/semanticTokens/browser/documentSemanticTokens'; + +suite('ModelSemanticColoring', () => { + + const disposables = new DisposableStore(); + let modelService: IModelService; + let languageService: ILanguageService; + let languageFeaturesService: ILanguageFeaturesService; + + setup(() => { + const configService = new TestConfigurationService({ editor: { semanticHighlighting: true } }); + const themeService = new TestThemeService(); + themeService.setTheme(new TestColorTheme({}, ColorScheme.DARK, true)); + const logService = new NullLogService(); + languageFeaturesService = new LanguageFeaturesService(); + languageService = disposables.add(new LanguageService(false)); + const semanticTokensStylingService = disposables.add(new SemanticTokensStylingService(themeService, logService, languageService)); + modelService = disposables.add(new ModelService( + configService, + new TestTextResourcePropertiesService(configService), + new UndoRedoService(new TestDialogService(), new TestNotificationService()), + languageService, + new TestLanguageConfigurationService(), + )); + disposables.add(new DocumentSemanticTokensFeature(semanticTokensStylingService, modelService, themeService, configService, new LanguageFeatureDebounceService(logService), languageFeaturesService)); + }); + + teardown(() => { + disposables.clear(); + }); + + test('DocumentSemanticTokens should be fetched when the result is empty if there are pending changes', async () => { + await runWithFakedTimers({}, async () => { + + disposables.add(languageService.registerLanguage({ id: 'testMode' })); + + const inFirstCall = new Barrier(); + const delayFirstResult = new Barrier(); + const secondResultProvided = new Barrier(); + let callCount = 0; + + disposables.add(languageFeaturesService.documentSemanticTokensProvider.register('testMode', new class implements DocumentSemanticTokensProvider { + getLegend(): SemanticTokensLegend { + return { tokenTypes: ['class'], tokenModifiers: [] }; + } + async provideDocumentSemanticTokens(model: ITextModel, lastResultId: string | null, token: CancellationToken): Promise { + callCount++; + if (callCount === 1) { + assert.ok('called once'); + inFirstCall.open(); + await delayFirstResult.wait(); + await timeout(0); // wait for the simple scheduler to fire to check that we do actually get rescheduled + return null; + } + if (callCount === 2) { + assert.ok('called twice'); + secondResultProvided.open(); + return null; + } + assert.fail('Unexpected call'); + } + releaseDocumentSemanticTokens(resultId: string | undefined): void { + } + })); + + const textModel = disposables.add(modelService.createModel('Hello world', languageService.createById('testMode'))); + + // wait for the provider to be called + await inFirstCall.wait(); + + // the provider is now in the provide call + // change the text buffer while the provider is running + textModel.applyEdits([{ range: new Range(1, 1, 1, 1), text: 'x' }]); + + // let the provider finish its first result + delayFirstResult.open(); + + // we need to check that the provider is called again, even if it returns null + await secondResultProvided.wait(); + + // assert that it got called twice + assert.strictEqual(callCount, 2); + }); + }); + + test('issue #149412: VS Code hangs when bad semantic token data is received', async () => { + await runWithFakedTimers({}, async () => { + + disposables.add(languageService.registerLanguage({ id: 'testMode' })); + + let lastResult: SemanticTokens | SemanticTokensEdits | null = null; + + disposables.add(languageFeaturesService.documentSemanticTokensProvider.register('testMode', new class implements DocumentSemanticTokensProvider { + getLegend(): SemanticTokensLegend { + return { tokenTypes: ['class'], tokenModifiers: [] }; + } + async provideDocumentSemanticTokens(model: ITextModel, lastResultId: string | null, token: CancellationToken): Promise { + if (!lastResultId) { + // this is the first call + lastResult = { + resultId: '1', + data: new Uint32Array([4294967293, 0, 7, 16, 0, 1, 4, 3, 11, 1]) + }; + } else { + // this is the second call + lastResult = { + resultId: '2', + edits: [{ + start: 4294967276, + deleteCount: 0, + data: new Uint32Array([2, 0, 3, 11, 0]) + }] + }; + } + return lastResult; + } + releaseDocumentSemanticTokens(resultId: string | undefined): void { + } + })); + + const textModel = disposables.add(modelService.createModel('', languageService.createById('testMode'))); + + // wait for the semantic tokens to be fetched + await Event.toPromise(textModel.onDidChangeTokens); + assert.strictEqual(lastResult!.resultId, '1'); + + // edit the text + textModel.applyEdits([{ range: new Range(1, 1, 1, 1), text: 'foo' }]); + + // wait for the semantic tokens to be fetched again + await Event.toPromise(textModel.onDidChangeTokens); + assert.strictEqual(lastResult!.resultId, '2'); + }); + }); + + test('issue #161573: onDidChangeSemanticTokens doesn\'t consistently trigger provideDocumentSemanticTokens', async () => { + await runWithFakedTimers({}, async () => { + + disposables.add(languageService.registerLanguage({ id: 'testMode' })); + + const emitter = new Emitter(); + let requestCount = 0; + disposables.add(languageFeaturesService.documentSemanticTokensProvider.register('testMode', new class implements DocumentSemanticTokensProvider { + onDidChange = emitter.event; + getLegend(): SemanticTokensLegend { + return { tokenTypes: ['class'], tokenModifiers: [] }; + } + async provideDocumentSemanticTokens(model: ITextModel, lastResultId: string | null, token: CancellationToken): Promise { + requestCount++; + if (requestCount === 1) { + await timeout(1000); + // send a change event + emitter.fire(); + await timeout(1000); + return null; + } + return null; + } + releaseDocumentSemanticTokens(resultId: string | undefined): void { + } + })); + + disposables.add(modelService.createModel('', languageService.createById('testMode'))); + + await timeout(5000); + assert.deepStrictEqual(requestCount, 2); + }); + }); + + test('DocumentSemanticTokens should be pick the token provider with actual items', async () => { + await runWithFakedTimers({}, async () => { + + let callCount = 0; + disposables.add(languageService.registerLanguage({ id: 'testMode2' })); + disposables.add(languageFeaturesService.documentSemanticTokensProvider.register('testMode2', new class implements DocumentSemanticTokensProvider { + getLegend(): SemanticTokensLegend { + return { tokenTypes: ['class1'], tokenModifiers: [] }; + } + async provideDocumentSemanticTokens(model: ITextModel, lastResultId: string | null, token: CancellationToken): Promise { + callCount++; + // For a secondary request return a different value + if (lastResultId) { + return { + data: new Uint32Array([2, 1, 1, 1, 1, 0, 2, 1, 1, 1]) + }; + } + return { + resultId: '1', + data: new Uint32Array([0, 1, 1, 1, 1, 0, 2, 1, 1, 1]) + }; + } + releaseDocumentSemanticTokens(resultId: string | undefined): void { + } + })); + disposables.add(languageFeaturesService.documentSemanticTokensProvider.register('testMode2', new class implements DocumentSemanticTokensProvider { + getLegend(): SemanticTokensLegend { + return { tokenTypes: ['class2'], tokenModifiers: [] }; + } + async provideDocumentSemanticTokens(model: ITextModel, lastResultId: string | null, token: CancellationToken): Promise { + callCount++; + return null; + } + releaseDocumentSemanticTokens(resultId: string | undefined): void { + } + })); + + function toArr(arr: Uint32Array): number[] { + const result: number[] = []; + for (let i = 0; i < arr.length; i++) { + result[i] = arr[i]; + } + return result; + } + + const textModel = modelService.createModel('Hello world 2', languageService.createById('testMode2')); + try { + let result = await getDocumentSemanticTokens(languageFeaturesService.documentSemanticTokensProvider, textModel, null, null, CancellationToken.None); + assert.ok(result, `We should have tokens (1)`); + assert.ok(result.tokens, `Tokens are found from multiple providers (1)`); + assert.ok(isSemanticTokens(result.tokens), `Tokens are full (1)`); + assert.ok(result.tokens.resultId, `Token result id found from multiple providers (1)`); + assert.deepStrictEqual(toArr(result.tokens.data), [0, 1, 1, 1, 1, 0, 2, 1, 1, 1], `Token data returned for multiple providers (1)`); + assert.deepStrictEqual(callCount, 2, `Called both token providers (1)`); + assert.deepStrictEqual(result.provider.getLegend(), { tokenTypes: ['class1'], tokenModifiers: [] }, `Legend matches the tokens (1)`); + + // Make a second request. Make sure we get the secondary value + result = await getDocumentSemanticTokens(languageFeaturesService.documentSemanticTokensProvider, textModel, result.provider, result.tokens.resultId, CancellationToken.None); + assert.ok(result, `We should have tokens (2)`); + assert.ok(result.tokens, `Tokens are found from multiple providers (2)`); + assert.ok(isSemanticTokens(result.tokens), `Tokens are full (2)`); + assert.ok(!result.tokens.resultId, `Token result id found from multiple providers (2)`); + assert.deepStrictEqual(toArr(result.tokens.data), [2, 1, 1, 1, 1, 0, 2, 1, 1, 1], `Token data returned for multiple providers (2)`); + assert.deepStrictEqual(callCount, 4, `Called both token providers (2)`); + assert.deepStrictEqual(result.provider.getLegend(), { tokenTypes: ['class1'], tokenModifiers: [] }, `Legend matches the tokens (2)`); + } finally { + disposables.clear(); + + // Wait for scheduler to finish + await timeout(0); + + // Now dispose the text model + textModel.dispose(); + } + }); + }); +}); diff --git a/src/vs/editor/test/common/services/getSemanticTokens.test.ts b/src/vs/editor/contrib/semanticTokens/test/browser/getSemanticTokens.test.ts similarity index 94% rename from src/vs/editor/test/common/services/getSemanticTokens.test.ts rename to src/vs/editor/contrib/semanticTokens/test/browser/getSemanticTokens.test.ts index aff59d80eea..8cb75776638 100644 --- a/src/vs/editor/test/common/services/getSemanticTokens.test.ts +++ b/src/vs/editor/contrib/semanticTokens/test/browser/getSemanticTokens.test.ts @@ -9,7 +9,7 @@ import { canceled } from 'vs/base/common/errors'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { ITextModel } from 'vs/editor/common/model'; import { DocumentSemanticTokensProvider, ProviderResult, SemanticTokens, SemanticTokensEdits, SemanticTokensLegend } from 'vs/editor/common/languages'; -import { getDocumentSemanticTokens } from 'vs/editor/common/services/getSemanticTokens'; +import { getDocumentSemanticTokens } from 'vs/editor/contrib/semanticTokens/common/getSemanticTokens'; import { createTextModel } from 'vs/editor/test/common/testTextModel'; import { LanguageFeatureRegistry } from 'vs/editor/common/languageFeatureRegistry'; diff --git a/src/vs/editor/contrib/stickyScroll/browser/stickyScroll.css b/src/vs/editor/contrib/stickyScroll/browser/stickyScroll.css index e030b549b25..a303d5f4945 100644 --- a/src/vs/editor/contrib/stickyScroll/browser/stickyScroll.css +++ b/src/vs/editor/contrib/stickyScroll/browser/stickyScroll.css @@ -4,36 +4,41 @@ *--------------------------------------------------------------------------------------------*/ .monaco-editor .sticky-line { - color : var(--vscode-editorLineNumber-foreground); - overflow : hidden; - white-space : nowrap; - display : inline-block; + color: var(--vscode-editorLineNumber-foreground); + overflow: hidden; + white-space: nowrap; + display: inline-block; } .monaco-editor .sticky-line-number { - text-align : right; - float : left; + text-align: right; + float: left; } .monaco-editor .sticky-line-root { - background-color : inherit; - overflow : hidden; - white-space : nowrap; - width : 100%; + background-color: inherit; + overflow: hidden; + white-space: nowrap; + width: 100%; +} + +.monaco-editor.hc-black .sticky-widget, +.monaco-editor.hc-white .sticky-widget { + border-bottom: 1px solid var(--vscode-contrastBorder); } .monaco-editor .sticky-line-root:hover { background-color: var(--vscode-editorStickyScrollHover-background); - cursor : pointer; + cursor: pointer; } .monaco-editor .sticky-widget { - width : 100%; - box-shadow : var(--vscode-scrollbar-shadow) 0 3px 2px -2px; - z-index : 11; - background-color : var(--vscode-editorStickyScroll-background); + width: 100%; + box-shadow: var(--vscode-scrollbar-shadow) 0 3px 2px -2px; + z-index: 11; + background-color: var(--vscode-editorStickyScroll-background); } .monaco-editor .sticky-widget.peek { - background-color : var(--vscode-peekViewEditorStickyScroll-background); + background-color: var(--vscode-peekViewEditorStickyScroll-background); } diff --git a/src/vs/editor/contrib/stickyScroll/browser/stickyScrollWidget.ts b/src/vs/editor/contrib/stickyScroll/browser/stickyScrollWidget.ts index 063f5f84b78..0daf442c877 100644 --- a/src/vs/editor/contrib/stickyScroll/browser/stickyScrollWidget.ts +++ b/src/vs/editor/contrib/stickyScroll/browser/stickyScrollWidget.ts @@ -283,6 +283,7 @@ export class StickyScrollWidget extends Disposable implements IOverlayWidget { } const editorLineHeight = this._editor.getOption(EditorOption.lineHeight); const widgetHeight: number = this._lineNumbers.length * editorLineHeight + this._lastLineRelativePosition; + this._rootDomNode.style.display = widgetHeight > 0 ? 'block' : 'none'; this._rootDomNode.style.height = widgetHeight.toString() + 'px'; const minimapSide = this._editor.getOption(EditorOption.minimap).side; if (minimapSide === 'left') { diff --git a/src/vs/editor/contrib/toggleTabFocusMode/browser/toggleTabFocusMode.ts b/src/vs/editor/contrib/toggleTabFocusMode/browser/toggleTabFocusMode.ts index 6feef76d443..6d9f2a8c6a1 100644 --- a/src/vs/editor/contrib/toggleTabFocusMode/browser/toggleTabFocusMode.ts +++ b/src/vs/editor/contrib/toggleTabFocusMode/browser/toggleTabFocusMode.ts @@ -18,13 +18,14 @@ export class ToggleTabFocusModeAction extends Action2 { constructor() { super({ id: ToggleTabFocusModeAction.ID, - title: nls.localize({ key: 'toggle.tabMovesFocus', comment: ['Turn on/off use of tab key for moving focus around VS Code'] }, "Toggle Tab Key Moves Focus"), + title: { value: nls.localize({ key: 'toggle.tabMovesFocus', comment: ['Turn on/off use of tab key for moving focus around VS Code'] }, 'Toggle Tab Key Moves Focus'), original: 'Toggle Tab Key Moves Focus' }, precondition: undefined, keybinding: { primary: KeyMod.CtrlCmd | KeyCode.KeyM, mac: { primary: KeyMod.WinCtrl | KeyMod.Shift | KeyCode.KeyM }, weight: KeybindingWeight.EditorContrib - } + }, + f1: true }); } diff --git a/src/vs/editor/editor.all.ts b/src/vs/editor/editor.all.ts index 9c960331ab4..38a7a98a345 100644 --- a/src/vs/editor/editor.all.ts +++ b/src/vs/editor/editor.all.ts @@ -42,16 +42,17 @@ import 'vs/editor/contrib/longLinesHelper/browser/longLinesHelper'; import 'vs/editor/contrib/multicursor/browser/multicursor'; import 'vs/editor/contrib/parameterHints/browser/parameterHints'; import 'vs/editor/contrib/rename/browser/rename'; -import 'vs/editor/contrib/stickyScroll/browser/stickyScrollContribution'; +import 'vs/editor/contrib/semanticTokens/browser/documentSemanticTokens'; +import 'vs/editor/contrib/semanticTokens/browser/viewportSemanticTokens'; import 'vs/editor/contrib/smartSelect/browser/smartSelect'; import 'vs/editor/contrib/snippet/browser/snippetController2'; +import 'vs/editor/contrib/stickyScroll/browser/stickyScrollContribution'; import 'vs/editor/contrib/suggest/browser/suggestController'; import 'vs/editor/contrib/suggest/browser/suggestInlineCompletions'; import 'vs/editor/contrib/tokenization/browser/tokenization'; import 'vs/editor/contrib/toggleTabFocusMode/browser/toggleTabFocusMode'; import 'vs/editor/contrib/unicodeHighlighter/browser/unicodeHighlighter'; import 'vs/editor/contrib/unusualLineTerminators/browser/unusualLineTerminators'; -import 'vs/editor/contrib/viewportSemanticTokens/browser/viewportSemanticTokens'; import 'vs/editor/contrib/wordHighlighter/browser/wordHighlighter'; import 'vs/editor/contrib/wordOperations/browser/wordOperations'; import 'vs/editor/contrib/wordPartOperations/browser/wordPartOperations'; diff --git a/src/vs/editor/standalone/browser/standaloneServices.ts b/src/vs/editor/standalone/browser/standaloneServices.ts index 3aeae6d6fc5..5a9653308cf 100644 --- a/src/vs/editor/standalone/browser/standaloneServices.ts +++ b/src/vs/editor/standalone/browser/standaloneServices.ts @@ -8,6 +8,8 @@ import 'vs/editor/standalone/browser/standaloneCodeEditorService'; import 'vs/editor/standalone/browser/standaloneLayoutService'; import 'vs/platform/undoRedo/common/undoRedoService'; import 'vs/editor/common/services/languageFeatureDebounce'; +import 'vs/editor/common/services/semanticTokensStylingService'; +import 'vs/editor/common/services/languageFeaturesService'; import * as strings from 'vs/base/common/strings'; import * as dom from 'vs/base/browser/dom'; @@ -84,12 +86,12 @@ import { MarkerService } from 'vs/platform/markers/common/markerService'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { IStorageService, InMemoryStorageService } from 'vs/platform/storage/common/storage'; - -import 'vs/editor/common/services/languageFeaturesService'; import { DefaultConfiguration } from 'vs/platform/configuration/common/configurations'; import { WorkspaceEdit } from 'vs/editor/common/languages'; import { AudioCue, IAudioCueService, Sound } from 'vs/platform/audioCues/browser/audioCueService'; import { LogService } from 'vs/platform/log/common/logService'; +import { getEditorFeatures } from 'vs/editor/common/editorFeatures'; +import { onUnexpectedError } from 'vs/base/common/errors'; class SimpleModel implements IResolvedTextEditorModel { @@ -1125,6 +1127,16 @@ export module StandaloneServices { } } + // Instantiate all editor features + const editorFeatures = getEditorFeatures(); + for (const feature of editorFeatures) { + try { + instantiationService.createInstance(feature); + } catch (err) { + onUnexpectedError(err); + } + } + return instantiationService; } } diff --git a/src/vs/editor/test/common/services/modelService.test.ts b/src/vs/editor/test/common/services/modelService.test.ts index 04fa4cc1362..07c303ba1b1 100644 --- a/src/vs/editor/test/common/services/modelService.test.ts +++ b/src/vs/editor/test/common/services/modelService.test.ts @@ -5,38 +5,19 @@ import * as assert from 'assert'; import { CharCode } from 'vs/base/common/charCode'; -import { Emitter, Event } from 'vs/base/common/event'; import * as platform from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; import { EditOperation } from 'vs/editor/common/core/editOperation'; import { Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; import { StringBuilder } from 'vs/editor/common/core/stringBuilder'; -import { DefaultEndOfLine, ITextModel } from 'vs/editor/common/model'; +import { DefaultEndOfLine } from 'vs/editor/common/model'; import { createTextBuffer } from 'vs/editor/common/model/textModel'; import { ModelService } from 'vs/editor/common/services/modelService'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; -import { TestColorTheme, TestThemeService } from 'vs/platform/theme/test/common/testThemeService'; -import { NullLogService } from 'vs/platform/log/common/log'; -import { UndoRedoService } from 'vs/platform/undoRedo/common/undoRedoService'; -import { TestDialogService } from 'vs/platform/dialogs/test/common/testDialogService'; -import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService'; import { createModelServices, createTextModel } from 'vs/editor/test/common/testTextModel'; import { DisposableStore } from 'vs/base/common/lifecycle'; -import { DocumentSemanticTokensProvider, SemanticTokens, SemanticTokensEdits, SemanticTokensLegend } from 'vs/editor/common/languages'; -import { CancellationToken } from 'vs/base/common/cancellation'; -import { Barrier, timeout } from 'vs/base/common/async'; -import { LanguageService } from 'vs/editor/common/services/languageService'; -import { ColorScheme } from 'vs/platform/theme/common/theme'; import { IModelService } from 'vs/editor/common/services/model'; -import { ILanguageService } from 'vs/editor/common/languages/language'; -import { TestTextResourcePropertiesService } from 'vs/editor/test/common/services/testTextResourcePropertiesService'; -import { TestLanguageConfigurationService } from 'vs/editor/test/common/modes/testLanguageConfigurationService'; -import { getDocumentSemanticTokens, isSemanticTokens } from 'vs/editor/common/services/getSemanticTokens'; -import { LanguageFeatureDebounceService } from 'vs/editor/common/services/languageFeatureDebounce'; -import { runWithFakedTimers } from 'vs/base/test/common/timeTravelScheduler'; -import { LanguageFeaturesService } from 'vs/editor/common/services/languageFeaturesService'; -import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; @@ -408,253 +389,6 @@ suite('ModelService', () => { }); }); -suite('ModelSemanticColoring', () => { - - const disposables = new DisposableStore(); - let modelService: IModelService; - let languageService: ILanguageService; - let languageFeaturesService: ILanguageFeaturesService; - - setup(() => { - const configService = new TestConfigurationService({ editor: { semanticHighlighting: true } }); - const themeService = new TestThemeService(); - themeService.setTheme(new TestColorTheme({}, ColorScheme.DARK, true)); - const logService = new NullLogService(); - languageFeaturesService = new LanguageFeaturesService(); - modelService = disposables.add(new ModelService( - configService, - new TestTextResourcePropertiesService(configService), - themeService, - logService, - new UndoRedoService(new TestDialogService(), new TestNotificationService()), - disposables.add(new LanguageService()), - new TestLanguageConfigurationService(), - new LanguageFeatureDebounceService(logService), - languageFeaturesService - )); - languageService = disposables.add(new LanguageService(false)); - }); - - teardown(() => { - disposables.clear(); - }); - - test('DocumentSemanticTokens should be fetched when the result is empty if there are pending changes', async () => { - await runWithFakedTimers({}, async () => { - - disposables.add(languageService.registerLanguage({ id: 'testMode' })); - - const inFirstCall = new Barrier(); - const delayFirstResult = new Barrier(); - const secondResultProvided = new Barrier(); - let callCount = 0; - - disposables.add(languageFeaturesService.documentSemanticTokensProvider.register('testMode', new class implements DocumentSemanticTokensProvider { - getLegend(): SemanticTokensLegend { - return { tokenTypes: ['class'], tokenModifiers: [] }; - } - async provideDocumentSemanticTokens(model: ITextModel, lastResultId: string | null, token: CancellationToken): Promise { - callCount++; - if (callCount === 1) { - assert.ok('called once'); - inFirstCall.open(); - await delayFirstResult.wait(); - await timeout(0); // wait for the simple scheduler to fire to check that we do actually get rescheduled - return null; - } - if (callCount === 2) { - assert.ok('called twice'); - secondResultProvided.open(); - return null; - } - assert.fail('Unexpected call'); - } - releaseDocumentSemanticTokens(resultId: string | undefined): void { - } - })); - - const textModel = disposables.add(modelService.createModel('Hello world', languageService.createById('testMode'))); - - // wait for the provider to be called - await inFirstCall.wait(); - - // the provider is now in the provide call - // change the text buffer while the provider is running - textModel.applyEdits([{ range: new Range(1, 1, 1, 1), text: 'x' }]); - - // let the provider finish its first result - delayFirstResult.open(); - - // we need to check that the provider is called again, even if it returns null - await secondResultProvided.wait(); - - // assert that it got called twice - assert.strictEqual(callCount, 2); - }); - }); - - test('issue #149412: VS Code hangs when bad semantic token data is received', async () => { - await runWithFakedTimers({}, async () => { - - disposables.add(languageService.registerLanguage({ id: 'testMode' })); - - let lastResult: SemanticTokens | SemanticTokensEdits | null = null; - - disposables.add(languageFeaturesService.documentSemanticTokensProvider.register('testMode', new class implements DocumentSemanticTokensProvider { - getLegend(): SemanticTokensLegend { - return { tokenTypes: ['class'], tokenModifiers: [] }; - } - async provideDocumentSemanticTokens(model: ITextModel, lastResultId: string | null, token: CancellationToken): Promise { - if (!lastResultId) { - // this is the first call - lastResult = { - resultId: '1', - data: new Uint32Array([4294967293, 0, 7, 16, 0, 1, 4, 3, 11, 1]) - }; - } else { - // this is the second call - lastResult = { - resultId: '2', - edits: [{ - start: 4294967276, - deleteCount: 0, - data: new Uint32Array([2, 0, 3, 11, 0]) - }] - }; - } - return lastResult; - } - releaseDocumentSemanticTokens(resultId: string | undefined): void { - } - })); - - const textModel = disposables.add(modelService.createModel('', languageService.createById('testMode'))); - - // wait for the semantic tokens to be fetched - await Event.toPromise(textModel.onDidChangeTokens); - assert.strictEqual(lastResult!.resultId, '1'); - - // edit the text - textModel.applyEdits([{ range: new Range(1, 1, 1, 1), text: 'foo' }]); - - // wait for the semantic tokens to be fetched again - await Event.toPromise(textModel.onDidChangeTokens); - assert.strictEqual(lastResult!.resultId, '2'); - }); - }); - - test('issue #161573: onDidChangeSemanticTokens doesn\'t consistently trigger provideDocumentSemanticTokens', async () => { - await runWithFakedTimers({}, async () => { - - disposables.add(languageService.registerLanguage({ id: 'testMode' })); - - const emitter = new Emitter(); - let requestCount = 0; - disposables.add(languageFeaturesService.documentSemanticTokensProvider.register('testMode', new class implements DocumentSemanticTokensProvider { - onDidChange = emitter.event; - getLegend(): SemanticTokensLegend { - return { tokenTypes: ['class'], tokenModifiers: [] }; - } - async provideDocumentSemanticTokens(model: ITextModel, lastResultId: string | null, token: CancellationToken): Promise { - requestCount++; - if (requestCount === 1) { - await timeout(1000); - // send a change event - emitter.fire(); - await timeout(1000); - return null; - } - return null; - } - releaseDocumentSemanticTokens(resultId: string | undefined): void { - } - })); - - disposables.add(modelService.createModel('', languageService.createById('testMode'))); - - await timeout(5000); - assert.deepStrictEqual(requestCount, 2); - }); - }); - - test('DocumentSemanticTokens should be pick the token provider with actual items', async () => { - await runWithFakedTimers({}, async () => { - - let callCount = 0; - disposables.add(languageService.registerLanguage({ id: 'testMode2' })); - disposables.add(languageFeaturesService.documentSemanticTokensProvider.register('testMode2', new class implements DocumentSemanticTokensProvider { - getLegend(): SemanticTokensLegend { - return { tokenTypes: ['class1'], tokenModifiers: [] }; - } - async provideDocumentSemanticTokens(model: ITextModel, lastResultId: string | null, token: CancellationToken): Promise { - callCount++; - // For a secondary request return a different value - if (lastResultId) { - return { - data: new Uint32Array([2, 1, 1, 1, 1, 0, 2, 1, 1, 1]) - }; - } - return { - resultId: '1', - data: new Uint32Array([0, 1, 1, 1, 1, 0, 2, 1, 1, 1]) - }; - } - releaseDocumentSemanticTokens(resultId: string | undefined): void { - } - })); - disposables.add(languageFeaturesService.documentSemanticTokensProvider.register('testMode2', new class implements DocumentSemanticTokensProvider { - getLegend(): SemanticTokensLegend { - return { tokenTypes: ['class2'], tokenModifiers: [] }; - } - async provideDocumentSemanticTokens(model: ITextModel, lastResultId: string | null, token: CancellationToken): Promise { - callCount++; - return null; - } - releaseDocumentSemanticTokens(resultId: string | undefined): void { - } - })); - - function toArr(arr: Uint32Array): number[] { - const result: number[] = []; - for (let i = 0; i < arr.length; i++) { - result[i] = arr[i]; - } - return result; - } - - const textModel = modelService.createModel('Hello world 2', languageService.createById('testMode2')); - try { - let result = await getDocumentSemanticTokens(languageFeaturesService.documentSemanticTokensProvider, textModel, null, null, CancellationToken.None); - assert.ok(result, `We should have tokens (1)`); - assert.ok(result.tokens, `Tokens are found from multiple providers (1)`); - assert.ok(isSemanticTokens(result.tokens), `Tokens are full (1)`); - assert.ok(result.tokens.resultId, `Token result id found from multiple providers (1)`); - assert.deepStrictEqual(toArr(result.tokens.data), [0, 1, 1, 1, 1, 0, 2, 1, 1, 1], `Token data returned for multiple providers (1)`); - assert.deepStrictEqual(callCount, 2, `Called both token providers (1)`); - assert.deepStrictEqual(result.provider.getLegend(), { tokenTypes: ['class1'], tokenModifiers: [] }, `Legend matches the tokens (1)`); - - // Make a second request. Make sure we get the secondary value - result = await getDocumentSemanticTokens(languageFeaturesService.documentSemanticTokensProvider, textModel, result.provider, result.tokens.resultId, CancellationToken.None); - assert.ok(result, `We should have tokens (2)`); - assert.ok(result.tokens, `Tokens are found from multiple providers (2)`); - assert.ok(isSemanticTokens(result.tokens), `Tokens are full (2)`); - assert.ok(!result.tokens.resultId, `Token result id found from multiple providers (2)`); - assert.deepStrictEqual(toArr(result.tokens.data), [2, 1, 1, 1, 1, 0, 2, 1, 1, 1], `Token data returned for multiple providers (2)`); - assert.deepStrictEqual(callCount, 4, `Called both token providers (2)`); - assert.deepStrictEqual(result.provider.getLegend(), { tokenTypes: ['class1'], tokenModifiers: [] }, `Legend matches the tokens (2)`); - } finally { - disposables.clear(); - - // Wait for scheduler to finish - await timeout(0); - - // Now dispose the text model - textModel.dispose(); - } - }); - }); -}); - function assertComputeEdits(lines1: string[], lines2: string[]): void { const model = createTextModel(lines1.join('\n')); const textBuffer = createTextBuffer(lines2.join('\n'), DefaultEndOfLine.LF).textBuffer; diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index f8c008e67db..ea87b12a731 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -3666,6 +3666,10 @@ declare namespace monaco.editor { * When enabled, this shows a preview of the drop location and triggers an `onDropIntoEditor` event. */ dropIntoEditor?: IDropIntoEditorOptions; + /** + * Controls whether the editor receives tabs or defers them to the workbench for navigation. + */ + tabFocusMode?: boolean; } export interface IDiffEditorBaseOptions { @@ -5612,7 +5616,8 @@ declare namespace monaco.editor { getDecorationsInRange(range: Range): IModelDecoration[] | null; /** * All decorations added through this call will get the ownerId of this editor. - * @deprecated + * @deprecated Use `createDecorationsCollection` + * @see createDecorationsCollection */ deltaDecorations(oldDecorations: string[], newDecorations: IModelDeltaDecoration[]): string[]; /** @@ -6409,16 +6414,6 @@ declare namespace monaco.languages { removeText?: number; } - export interface IBackgroundTokenizer extends IDisposable { - /** - * Instructs the background tokenizer to set the tokens for the given range again. - * - * This might be necessary if the renderer overwrote those tokens with heuristically computed ones for some viewport, - * when the change does not even propagate to that viewport. - */ - requestTokens(startLineNumber: number, endLineNumberExclusive: number): void; - } - /** * The state of the tokenizer between two lines. * It is useful to store flags such as in multiline comment, etc. diff --git a/src/vs/platform/audioCues/browser/audioCueService.ts b/src/vs/platform/audioCues/browser/audioCueService.ts index fa212de0883..8f4a1384e58 100644 --- a/src/vs/platform/audioCues/browser/audioCueService.ts +++ b/src/vs/platform/audioCues/browser/audioCueService.ts @@ -294,6 +294,12 @@ export class AudioCue { settingsKey: 'audioCues.taskFailed' }); + public static readonly terminalCommandFailed = AudioCue.register({ + name: localize('audioCues.terminalCommandFailed', 'Terminal Command Failed'), + sound: Sound.taskFailed, + settingsKey: 'audioCues.terminalCommandFailed' + }); + public static readonly terminalBell = AudioCue.register({ name: localize('audioCues.terminalBell', 'Terminal Bell'), sound: Sound.terminalBell, diff --git a/src/vs/platform/contextkey/common/contextkey.ts b/src/vs/platform/contextkey/common/contextkey.ts index 111cf4bce1c..87d054af797 100644 --- a/src/vs/platform/contextkey/common/contextkey.ts +++ b/src/vs/platform/contextkey/common/contextkey.ts @@ -124,50 +124,50 @@ export abstract class ContextKeyExpr { return ContextKeySmallerEqualsExpr.create(key, value); } - public static deserialize(serialized: string | null | undefined, strict: boolean = false): ContextKeyExpression | undefined { + public static deserialize(serialized: string | null | undefined): ContextKeyExpression | undefined { if (!serialized) { return undefined; } - return this._deserializeOrExpression(serialized, strict); + return this._deserializeOrExpression(serialized); } - private static _deserializeOrExpression(serialized: string, strict: boolean): ContextKeyExpression | undefined { + private static _deserializeOrExpression(serialized: string): ContextKeyExpression | undefined { const pieces = serialized.split('||'); - return ContextKeyOrExpr.create(pieces.map(p => this._deserializeAndExpression(p, strict)), null, true); + return ContextKeyOrExpr.create(pieces.map(p => this._deserializeAndExpression(p)), null, true); } - private static _deserializeAndExpression(serialized: string, strict: boolean): ContextKeyExpression | undefined { + private static _deserializeAndExpression(serialized: string): ContextKeyExpression | undefined { const pieces = serialized.split('&&'); - return ContextKeyAndExpr.create(pieces.map(p => this._deserializeOne(p, strict)), null, true); + return ContextKeyAndExpr.create(pieces.map(p => this._deserializeOne(p)), null, true); } - private static _deserializeOne(serializedOne: string, strict: boolean): ContextKeyExpression { + private static _deserializeOne(serializedOne: string): ContextKeyExpression { serializedOne = serializedOne.trim(); if (serializedOne.indexOf('!=') >= 0) { const pieces = serializedOne.split('!='); - return ContextKeyNotEqualsExpr.create(pieces[0].trim(), this._deserializeValue(pieces[1], strict)); + return ContextKeyNotEqualsExpr.create(pieces[0].trim(), this._deserializeValue(pieces[1])); } if (serializedOne.indexOf('==') >= 0) { const pieces = serializedOne.split('=='); - return ContextKeyEqualsExpr.create(pieces[0].trim(), this._deserializeValue(pieces[1], strict)); + return ContextKeyEqualsExpr.create(pieces[0].trim(), this._deserializeValue(pieces[1])); } if (serializedOne.indexOf('=~') >= 0) { const pieces = serializedOne.split('=~'); - return ContextKeyRegexExpr.create(pieces[0].trim(), this._deserializeRegexValue(pieces[1], strict)); + return ContextKeyRegexExpr.create(pieces[0].trim(), this._deserializeRegexValue(pieces[1])); } - if (serializedOne.indexOf(' not in ') >= 0) { + if (serializedOne.indexOf(' not in ') >= 0) { // careful: this must come before `in` const pieces = serializedOne.split(' not in '); - return ContextKeyNotInExpr.create(pieces[0].trim(), this._deserializeValue(pieces[1], strict)); + return ContextKeyNotInExpr.create(pieces[0].trim(), this._deserializeValue(pieces[1])); } if (serializedOne.indexOf(' in ') >= 0) { const pieces = serializedOne.split(' in '); - return ContextKeyInExpr.create(pieces[0].trim(), this._deserializeValue(pieces[1], strict)); + return ContextKeyInExpr.create(pieces[0].trim(), this._deserializeValue(pieces[1])); } if (/^[^<=>]+>=[^<=>]+$/.test(serializedOne)) { @@ -197,7 +197,7 @@ export abstract class ContextKeyExpr { return ContextKeyDefinedExpr.create(serializedOne); } - private static _deserializeValue(serializedValue: string, strict: boolean): any { + private static _deserializeValue(serializedValue: string): any { serializedValue = serializedValue.trim(); if (serializedValue === 'true') { @@ -216,25 +216,17 @@ export abstract class ContextKeyExpr { return serializedValue; } - private static _deserializeRegexValue(serializedValue: string, strict: boolean): RegExp | null { + private static _deserializeRegexValue(serializedValue: string): RegExp | null { if (isFalsyOrWhitespace(serializedValue)) { - if (strict) { - throw new Error('missing regexp-value for =~-expression'); - } else { - console.warn('missing regexp-value for =~-expression'); - } + console.warn('missing regexp-value for =~-expression'); return null; } const start = serializedValue.indexOf('/'); const end = serializedValue.lastIndexOf('/'); if (start === end || start < 0 /* || to < 0 */) { - if (strict) { - throw new Error(`bad regexp-value '${serializedValue}', missing /-enclosure`); - } else { - console.warn(`bad regexp-value '${serializedValue}', missing /-enclosure`); - } + console.warn(`bad regexp-value '${serializedValue}', missing /-enclosure`); return null; } @@ -243,11 +235,7 @@ export abstract class ContextKeyExpr { try { return new RegExp(value, caseIgnoreFlag); } catch (e) { - if (strict) { - throw new Error(`bad regexp-value '${serializedValue}', parse error: ${e}`); - } else { - console.warn(`bad regexp-value '${serializedValue}', parse error: ${e}`); - } + console.warn(`bad regexp-value '${serializedValue}', parse error: ${e}`); return null; } } diff --git a/src/vs/platform/environment/common/argv.ts b/src/vs/platform/environment/common/argv.ts index a932ea6ec5d..ed567c6ca6f 100644 --- a/src/vs/platform/environment/common/argv.ts +++ b/src/vs/platform/environment/common/argv.ts @@ -67,6 +67,8 @@ export interface NativeParsedArgs { 'inspect-brk-search'?: string; 'inspect-ptyhost'?: string; 'inspect-brk-ptyhost'?: string; + 'inspect-sharedprocess'?: string; + 'inspect-brk-sharedprocess'?: string; 'disable-extensions'?: boolean; 'disable-extension'?: string[]; // undefined or array of 1 or more 'list-extensions'?: boolean; diff --git a/src/vs/platform/environment/common/environmentService.ts b/src/vs/platform/environment/common/environmentService.ts index 0bd3493cbd5..b724b7a6a61 100644 --- a/src/vs/platform/environment/common/environmentService.ts +++ b/src/vs/platform/environment/common/environmentService.ts @@ -11,7 +11,7 @@ import { env } from 'vs/base/common/process'; import { joinPath } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; -import { ExtensionKind, IDebugParams, IExtensionHostDebugParams, INativeEnvironmentService } from 'vs/platform/environment/common/environment'; +import { ExtensionKind, IExtensionHostDebugParams, INativeEnvironmentService } from 'vs/platform/environment/common/environment'; import { IProductService } from 'vs/platform/product/common/productService'; export const EXTENSION_IDENTIFIER_WITH_LOG_REGEX = /^([^.]+\..+):(.+)$/; @@ -213,7 +213,7 @@ export abstract class AbstractNativeEnvironmentService implements INativeEnviron } @memoize - get debugExtensionHost(): IExtensionHostDebugParams { return parseExtensionHostPort(this.args, this.isBuilt); } + get debugExtensionHost(): IExtensionHostDebugParams { return parseExtensionHostDebugPort(this.args, this.isBuilt); } get debugRenderer(): boolean { return !!this.args.debugRenderer; } get isBuilt(): boolean { return !env['VSCODE_DEV']; } @@ -278,17 +278,13 @@ export abstract class AbstractNativeEnvironmentService implements INativeEnviron ) { } } -export function parseExtensionHostPort(args: NativeParsedArgs, isBuild: boolean): IExtensionHostDebugParams { +export function parseExtensionHostDebugPort(args: NativeParsedArgs, isBuild: boolean): IExtensionHostDebugParams { return parseDebugParams(args['inspect-extensions'], args['inspect-brk-extensions'], 5870, isBuild, args.debugId, args.extensionEnvironment); } -export function parsePtyHostPort(args: NativeParsedArgs, isBuild: boolean): IDebugParams { - return parseDebugParams(args['inspect-ptyhost'], args['inspect-brk-ptyhost'], 5877, isBuild, args.extensionEnvironment); -} - -function parseDebugParams(debugArg: string | undefined, debugBrkArg: string | undefined, defaultBuildPort: number, isBuild: boolean, debugId?: string, environmentString?: string): IExtensionHostDebugParams { +export function parseDebugParams(debugArg: string | undefined, debugBrkArg: string | undefined, defaultBuildPort: number, isBuilt: boolean, debugId?: string, environmentString?: string): IExtensionHostDebugParams { const portStr = debugBrkArg || debugArg; - const port = Number(portStr) || (!isBuild ? defaultBuildPort : null); + const port = Number(portStr) || (!isBuilt ? defaultBuildPort : null); const brk = port ? Boolean(!!debugBrkArg) : false; let env: Record | undefined; if (environmentString) { diff --git a/src/vs/platform/environment/node/argv.ts b/src/vs/platform/environment/node/argv.ts index 41454d12050..d972185f033 100644 --- a/src/vs/platform/environment/node/argv.ts +++ b/src/vs/platform/environment/node/argv.ts @@ -128,6 +128,8 @@ export const OPTIONS: OptionDescriptions> = { 'inspect-brk-ptyhost': { type: 'string', allowEmptyValue: true }, 'inspect-search': { type: 'string', deprecates: ['debugSearch'], allowEmptyValue: true }, 'inspect-brk-search': { type: 'string', deprecates: ['debugBrkSearch'], allowEmptyValue: true }, + 'inspect-sharedprocess': { type: 'string', allowEmptyValue: true }, + 'inspect-brk-sharedprocess': { type: 'string', allowEmptyValue: true }, 'export-default-configuration': { type: 'string' }, 'install-source': { type: 'string' }, 'enable-smoke-test-driver': { type: 'boolean' }, diff --git a/src/vs/platform/environment/node/environmentService.ts b/src/vs/platform/environment/node/environmentService.ts index cf59e2f135a..bdd756f429f 100644 --- a/src/vs/platform/environment/node/environmentService.ts +++ b/src/vs/platform/environment/node/environmentService.ts @@ -5,7 +5,8 @@ import { homedir, tmpdir } from 'os'; import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; -import { AbstractNativeEnvironmentService } from 'vs/platform/environment/common/environmentService'; +import { IDebugParams } from 'vs/platform/environment/common/environment'; +import { AbstractNativeEnvironmentService, parseDebugParams } from 'vs/platform/environment/common/environmentService'; import { getUserDataPath } from 'vs/platform/environment/node/userDataPath'; import { IProductService } from 'vs/platform/product/common/productService'; @@ -19,3 +20,11 @@ export class NativeEnvironmentService extends AbstractNativeEnvironmentService { }, productService); } } + +export function parsePtyHostDebugPort(args: NativeParsedArgs, isBuild: boolean): IDebugParams { + return parseDebugParams(args['inspect-ptyhost'], args['inspect-brk-ptyhost'], 5877, isBuild, args.extensionEnvironment); +} + +export function parseSharedProcessDebugPort(args: NativeParsedArgs, isBuild: boolean): IDebugParams { + return parseDebugParams(args['inspect-sharedprocess'], args['inspect-brk-sharedprocess'], 5879, isBuild, args.extensionEnvironment); +} diff --git a/src/vs/platform/environment/test/node/environmentService.test.ts b/src/vs/platform/environment/test/node/environmentService.test.ts index 183719c5fdd..85cb855f1c9 100644 --- a/src/vs/platform/environment/test/node/environmentService.test.ts +++ b/src/vs/platform/environment/test/node/environmentService.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { parseExtensionHostPort } from 'vs/platform/environment/common/environmentService'; +import { parseExtensionHostDebugPort } from 'vs/platform/environment/common/environmentService'; import { OPTIONS, parseArgs } from 'vs/platform/environment/node/argv'; import { NativeEnvironmentService } from 'vs/platform/environment/node/environmentService'; import product from 'vs/platform/product/common/product'; @@ -12,7 +12,7 @@ import product from 'vs/platform/product/common/product'; suite('EnvironmentService', () => { test('parseExtensionHostPort when built', () => { - const parse = (a: string[]) => parseExtensionHostPort(parseArgs(a, OPTIONS), true); + const parse = (a: string[]) => parseExtensionHostDebugPort(parseArgs(a, OPTIONS), true); assert.deepStrictEqual(parse([]), { port: null, break: false, env: undefined, debugId: undefined }); assert.deepStrictEqual(parse(['--debugPluginHost']), { port: null, break: false, env: undefined, debugId: undefined }); @@ -30,7 +30,7 @@ suite('EnvironmentService', () => { }); test('parseExtensionHostPort when unbuilt', () => { - const parse = (a: string[]) => parseExtensionHostPort(parseArgs(a, OPTIONS), false); + const parse = (a: string[]) => parseExtensionHostDebugPort(parseArgs(a, OPTIONS), false); assert.deepStrictEqual(parse([]), { port: 5870, break: false, env: undefined, debugId: undefined }); assert.deepStrictEqual(parse(['--debugPluginHost']), { port: 5870, break: false, env: undefined, debugId: undefined }); diff --git a/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts b/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts index 0fde9407ded..78eb1b06f24 100644 --- a/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts +++ b/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts @@ -653,13 +653,14 @@ export abstract class AbstractExtensionManagementService extends Disposable impl abstract getManifest(vsix: URI): Promise; abstract install(vsix: URI, options?: InstallVSIXOptions): Promise; abstract installFromLocation(location: URI, profileLocation: URI): Promise; + abstract installExtensionsFromProfile(extensions: IExtensionIdentifier[], fromProfileLocation: URI, toProfileLocation: URI): Promise; abstract getInstalled(type?: ExtensionType, profileLocation?: URI): Promise; + abstract copyExtensions(fromProfileLocation: URI, toProfileLocation: URI): Promise; abstract download(extension: IGalleryExtension, operation: InstallOperation): Promise; abstract reinstallFromGallery(extension: ILocalExtension): Promise; abstract cleanUp(): Promise; abstract onDidUpdateExtensionMetadata: Event; - abstract getMetadata(extension: ILocalExtension, profileLocation?: URI): Promise; abstract updateMetadata(local: ILocalExtension, metadata: Partial, profileLocation?: URI): Promise; protected abstract getCurrentExtensionsManifestLocation(): URI; diff --git a/src/vs/platform/extensionManagement/common/extensionManagement.ts b/src/vs/platform/extensionManagement/common/extensionManagement.ts index c5f01490d8b..2864289beba 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagement.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagement.ts @@ -454,12 +454,12 @@ export interface IExtensionManagementService { canInstall(extension: IGalleryExtension): Promise; installFromGallery(extension: IGalleryExtension, options?: InstallOptions): Promise; installFromLocation(location: URI, profileLocation: URI): Promise; + installExtensionsFromProfile(extensions: IExtensionIdentifier[], fromProfileLocation: URI, toProfileLocation: URI): Promise; uninstall(extension: ILocalExtension, options?: UninstallOptions): Promise; reinstallFromGallery(extension: ILocalExtension): Promise; getInstalled(type?: ExtensionType, profileLocation?: URI): Promise; getExtensionsControlManifest(): Promise; - - getMetadata(extension: ILocalExtension, profileLocation?: URI): Promise; + copyExtensions(fromProfileLocation: URI, toProfileLocation: URI): Promise; updateMetadata(local: ILocalExtension, metadata: Partial, profileLocation?: URI): Promise; download(extension: IGalleryExtension, operation: InstallOperation): Promise; diff --git a/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts b/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts index 0e5dcbceffc..81d3ceb46f9 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts @@ -5,7 +5,6 @@ import { Emitter, Event } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; -import { revive } from 'vs/base/common/marshalling'; import { cloneAndChange } from 'vs/base/common/objects'; import { URI, UriComponents } from 'vs/base/common/uri'; import { DefaultURITransformer, IURITransformer, transformAndReviveIncomingURIs } from 'vs/base/common/uriIpc'; @@ -13,8 +12,10 @@ import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc'; import { IExtensionIdentifier, IExtensionTipsService, IGalleryExtension, ILocalExtension, IExtensionsControlManifest, isTargetPlatformCompatible, InstallOptions, InstallVSIXOptions, UninstallOptions, Metadata, IExtensionManagementService, DidUninstallExtensionEvent, InstallExtensionEvent, InstallExtensionResult, UninstallExtensionEvent, InstallOperation } from 'vs/platform/extensionManagement/common/extensionManagement'; import { ExtensionType, IExtensionManifest, TargetPlatform } from 'vs/platform/extensions/common/extensions'; -function transformIncomingURI(uri: UriComponents, transformer: IURITransformer | null): URI { - return URI.revive(transformer ? transformer.transformIncoming(uri) : uri); +function transformIncomingURI(uri: UriComponents, transformer: IURITransformer | null): URI; +function transformIncomingURI(uri: UriComponents | undefined, transformer: IURITransformer | null): URI | undefined; +function transformIncomingURI(uri: UriComponents | undefined, transformer: IURITransformer | null): URI | undefined { + return uri ? URI.revive(transformer ? transformer.transformIncoming(uri) : uri) : undefined; } function transformOutgoingURI(uri: URI, transformer: IURITransformer | null): URI { @@ -28,6 +29,10 @@ function transformIncomingExtension(extension: ILocalExtension, transformer: IUR return { ...transformed, ...{ manifest } }; } +function transformIncomingOptions(options: O | undefined, transformer: IURITransformer | null): O | undefined { + return options?.profileLocation ? transformAndReviveIncomingURIs(options, transformer ?? DefaultURITransformer) : options; +} + function transformOutgoingExtension(extension: ILocalExtension, transformer: IURITransformer | null): ILocalExtension { return transformer ? cloneAndChange(extension, value => value instanceof URI ? transformer.transformOutgoingURI(value) : undefined) : extension; } @@ -51,41 +56,112 @@ export class ExtensionManagementChannel implements IServerChannel { listen(context: any, event: string): Event { const uriTransformer = this.getUriTransformer(context); switch (event) { - case 'onInstallExtension': return this.onInstallExtension; - case 'onDidInstallExtensions': return Event.map(this.onDidInstallExtensions, results => results.map(i => ({ ...i, local: i.local ? transformOutgoingExtension(i.local, uriTransformer) : i.local }))); - case 'onUninstallExtension': return this.onUninstallExtension; - case 'onDidUninstallExtension': return this.onDidUninstallExtension; - case 'onDidUpdateExtensionMetadata': return this.onDidUpdateExtensionMetadata; + case 'onInstallExtension': { + return Event.map(this.onInstallExtension, e => { + return { + ...e, + profileLocation: e.profileLocation ? transformOutgoingURI(e.profileLocation, uriTransformer) : e.profileLocation + }; + }); + } + case 'onDidInstallExtensions': { + return Event.map(this.onDidInstallExtensions, results => + results.map(i => ({ + ...i, + local: i.local ? transformOutgoingExtension(i.local, uriTransformer) : i.local, + profileLocation: i.profileLocation ? transformOutgoingURI(i.profileLocation, uriTransformer) : i.profileLocation + }))); + } + case 'onUninstallExtension': { + return Event.map(this.onUninstallExtension, e => { + return { + ...e, + profileLocation: e.profileLocation ? transformOutgoingURI(e.profileLocation, uriTransformer) : e.profileLocation + }; + }); + } + case 'onDidUninstallExtension': { + return Event.map(this.onDidUninstallExtension, e => { + return { + ...e, + profileLocation: e.profileLocation ? transformOutgoingURI(e.profileLocation, uriTransformer) : e.profileLocation + }; + }); + } + case 'onDidUpdateExtensionMetadata': { + return Event.map(this.onDidUpdateExtensionMetadata, e => transformOutgoingExtension(e, uriTransformer)); + } } throw new Error('Invalid listen'); } - call(context: any, command: string, args?: any): Promise { + async call(context: any, command: string, args?: any): Promise { const uriTransformer: IURITransformer | null = this.getUriTransformer(context); switch (command) { - case 'zip': return this.service.zip(transformIncomingExtension(args[0], uriTransformer)).then(uri => transformOutgoingURI(uri, uriTransformer)); - case 'unzip': return this.service.unzip(transformIncomingURI(args[0], uriTransformer)); - case 'install': return this.service.install(transformIncomingURI(args[0], uriTransformer), revive(args[1])); - case 'installFromLocation': return this.service.installFromLocation(transformIncomingURI(args[0], uriTransformer), URI.revive(args[1])); - case 'getManifest': return this.service.getManifest(transformIncomingURI(args[0], uriTransformer)); - case 'getTargetPlatform': return this.service.getTargetPlatform(); - case 'canInstall': return this.service.canInstall(args[0]); - case 'installFromGallery': return this.service.installFromGallery(args[0], revive(args[1])); - case 'uninstall': return this.service.uninstall(transformIncomingExtension(args[0], uriTransformer), revive(args[1])); - case 'reinstallFromGallery': return this.service.reinstallFromGallery(transformIncomingExtension(args[0], uriTransformer)); - case 'getInstalled': return this.service.getInstalled(args[0], URI.revive(args[1])).then(extensions => extensions.map(e => transformOutgoingExtension(e, uriTransformer))); - case 'getMetadata': return this.service.getMetadata(transformIncomingExtension(args[0], uriTransformer), URI.revive(args[1])); - case 'updateMetadata': return this.service.updateMetadata(transformIncomingExtension(args[0], uriTransformer), args[1], URI.revive(args[2])).then(e => transformOutgoingExtension(e, uriTransformer)); - case 'getExtensionsControlManifest': return this.service.getExtensionsControlManifest(); - case 'download': return this.service.download(args[0], args[1]); - case 'cleanUp': return this.service.cleanUp(); + case 'zip': { + const extension = transformIncomingExtension(args[0], uriTransformer); + const uri = await this.service.zip(extension); + return transformOutgoingURI(uri, uriTransformer); + } + case 'unzip': { + return this.service.unzip(transformIncomingURI(args[0], uriTransformer)); + } + case 'install': { + return this.service.install(transformIncomingURI(args[0], uriTransformer), transformIncomingOptions(args[1], uriTransformer)); + } + case 'installFromLocation': { + return this.service.installFromLocation(transformIncomingURI(args[0], uriTransformer), transformIncomingURI(args[1], uriTransformer)); + } + case 'installExtensionsFromProfile': { + return this.service.installExtensionsFromProfile(args[0], transformIncomingURI(args[1], uriTransformer), transformIncomingURI(args[2], uriTransformer)); + } + case 'getManifest': { + return this.service.getManifest(transformIncomingURI(args[0], uriTransformer)); + } + case 'getTargetPlatform': { + return this.service.getTargetPlatform(); + } + case 'canInstall': { + return this.service.canInstall(args[0]); + } + case 'installFromGallery': { + return this.service.installFromGallery(args[0], transformIncomingOptions(args[1], uriTransformer)); + } + case 'uninstall': { + return this.service.uninstall(transformIncomingExtension(args[0], uriTransformer), transformIncomingOptions(args[1], uriTransformer)); + } + case 'reinstallFromGallery': { + return this.service.reinstallFromGallery(transformIncomingExtension(args[0], uriTransformer)); + } + case 'getInstalled': { + const extensions = await this.service.getInstalled(args[0], transformIncomingURI(args[1], uriTransformer)); + return extensions.map(e => transformOutgoingExtension(e, uriTransformer)); + } + case 'copyExtensions': { + return this.service.copyExtensions(transformIncomingURI(args[0], uriTransformer), transformIncomingURI(args[1], uriTransformer)); + } + case 'updateMetadata': { + const e = await this.service.updateMetadata(transformIncomingExtension(args[0], uriTransformer), args[1], transformIncomingURI(args[2], uriTransformer)); + return transformOutgoingExtension(e, uriTransformer); + } + case 'getExtensionsControlManifest': { + return this.service.getExtensionsControlManifest(); + } + case 'download': { + return this.service.download(args[0], args[1]); + } + case 'cleanUp': { + return this.service.cleanUp(); + } } throw new Error('Invalid call'); } } +export type ExtensionEventResult = InstallExtensionEvent | InstallExtensionResult | UninstallExtensionEvent | DidUninstallExtensionEvent; + export class ExtensionManagementChannelClient extends Disposable implements IExtensionManagementService { declare readonly _serviceBrand: undefined; @@ -107,13 +183,23 @@ export class ExtensionManagementChannelClient extends Disposable implements IExt constructor(private readonly channel: IChannel) { super(); - this._register(this.channel.listen('onInstallExtension')(e => this._onInstallExtension.fire({ identifier: e.identifier, source: this.isUriComponents(e.source) ? URI.revive(e.source) : e.source, profileLocation: URI.revive(e.profileLocation) }))); - this._register(this.channel.listen('onDidInstallExtensions')(results => this._onDidInstallExtensions.fire(results.map(e => ({ ...e, local: e.local ? transformIncomingExtension(e.local, null) : e.local, source: this.isUriComponents(e.source) ? URI.revive(e.source) : e.source, profileLocation: URI.revive(e.profileLocation) }))))); - this._register(this.channel.listen('onUninstallExtension')(e => this._onUninstallExtension.fire({ identifier: e.identifier, profileLocation: URI.revive(e.profileLocation) }))); - this._register(this.channel.listen('onDidUninstallExtension')(e => this._onDidUninstallExtension.fire({ ...e, profileLocation: URI.revive(e.profileLocation) }))); + this._register(this.channel.listen('onInstallExtension')(e => this.fireEvent(this._onInstallExtension, { ...e, source: this.isUriComponents(e.source) ? URI.revive(e.source) : e.source, profileLocation: URI.revive(e.profileLocation) }))); + this._register(this.channel.listen('onDidInstallExtensions')(results => this.fireEvent(this._onDidInstallExtensions, results.map(e => ({ ...e, local: e.local ? transformIncomingExtension(e.local, null) : e.local, source: this.isUriComponents(e.source) ? URI.revive(e.source) : e.source, profileLocation: URI.revive(e.profileLocation) }))))); + this._register(this.channel.listen('onUninstallExtension')(e => this.fireEvent(this._onUninstallExtension, { ...e, profileLocation: URI.revive(e.profileLocation) }))); + this._register(this.channel.listen('onDidUninstallExtension')(e => this.fireEvent(this._onDidUninstallExtension, { ...e, profileLocation: URI.revive(e.profileLocation) }))); this._register(this.channel.listen('onDidUpdateExtensionMetadata')(e => this._onDidUpdateExtensionMetadata.fire(transformIncomingExtension(e, null)))); } + protected fireEvent(event: Emitter, data: InstallExtensionEvent): void; + protected fireEvent(event: Emitter, data: InstallExtensionResult[]): void; + protected fireEvent(event: Emitter, data: UninstallExtensionEvent): void; + protected fireEvent(event: Emitter, data: DidUninstallExtensionEvent): void; + protected fireEvent(event: Emitter, data: ExtensionEventResult): void; + protected fireEvent(event: Emitter, data: ExtensionEventResult[]): void; + protected fireEvent(event: Emitter, data: E): void { + event.fire(data); + } + private isUriComponents(thing: unknown): thing is UriComponents { if (!thing) { return false; @@ -151,6 +237,11 @@ export class ExtensionManagementChannelClient extends Disposable implements IExt return Promise.resolve(this.channel.call('installFromLocation', [location, profileLocation])).then(local => transformIncomingExtension(local, null)); } + async installExtensionsFromProfile(extensions: IExtensionIdentifier[], fromProfileLocation: URI, toProfileLocation: URI): Promise { + const result = await this.channel.call('installExtensionsFromProfile', [extensions, fromProfileLocation, toProfileLocation]); + return result.map(local => transformIncomingExtension(local, null)); + } + getManifest(vsix: URI): Promise { return Promise.resolve(this.channel.call('getManifest', [vsix])); } @@ -172,15 +263,15 @@ export class ExtensionManagementChannelClient extends Disposable implements IExt .then(extensions => extensions.map(extension => transformIncomingExtension(extension, null))); } - getMetadata(local: ILocalExtension, extensionsProfileResource?: URI): Promise { - return Promise.resolve(this.channel.call('getMetadata', [local, extensionsProfileResource])); - } - updateMetadata(local: ILocalExtension, metadata: Partial, extensionsProfileResource?: URI): Promise { return Promise.resolve(this.channel.call('updateMetadata', [local, metadata, extensionsProfileResource])) .then(extension => transformIncomingExtension(extension, null)); } + copyExtensions(fromProfileLocation: URI, toProfileLocation: URI): Promise { + return this.channel.call('copyExtensions', [fromProfileLocation, toProfileLocation]); + } + getExtensionsControlManifest(): Promise { return Promise.resolve(this.channel.call('getExtensionsControlManifest')); } diff --git a/src/vs/platform/extensionManagement/common/extensionsProfileScannerService.ts b/src/vs/platform/extensionManagement/common/extensionsProfileScannerService.ts index 0581b2ec357..d55c40f3611 100644 --- a/src/vs/platform/extensionManagement/common/extensionsProfileScannerService.ts +++ b/src/vs/platform/extensionManagement/common/extensionsProfileScannerService.ts @@ -14,12 +14,13 @@ import { areSameExtensions } from 'vs/platform/extensionManagement/common/extens import { IExtension, IExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { FileOperationResult, IFileService, toFileOperationResult } from 'vs/platform/files/common/files'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { ILogService } from 'vs/platform/log/common/log'; +import { ILogService, LogLevel } from 'vs/platform/log/common/log'; import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { Mutable, isObject, isString, isUndefined } from 'vs/base/common/types'; import { getErrorMessage } from 'vs/base/common/errors'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { Schemas } from 'vs/base/common/network'; interface IStoredProfileExtension { identifier: IExtensionIdentifier; @@ -248,7 +249,7 @@ export abstract class AbstractExtensionsProfileScannerService extends Disposable this.reportAndThrowInvalidConentError(file); } let location: URI; - if (isString(e.relativeLocation)) { + if (isString(e.relativeLocation) && e.relativeLocation) { // Extension in new format. No migration needed. location = this.resolveExtensionLocation(e.relativeLocation); } else if (isString(e.location)) { @@ -309,9 +310,14 @@ export abstract class AbstractExtensionsProfileScannerService extends Disposable } private toRelativePath(extensionLocation: URI): string | undefined { - return this.uriIdentityService.extUri.isEqualOrParent(extensionLocation, this.extensionsLocation) - ? this.uriIdentityService.extUri.relativePath(this.extensionsLocation, extensionLocation) - : undefined; + if (this.uriIdentityService.extUri.isEqualOrParent(extensionLocation, this.extensionsLocation)) { + const relativePath = this.uriIdentityService.extUri.relativePath(this.extensionsLocation, extensionLocation); + if (this.extensionsLocation.scheme === Schemas.file && this.logService.getLevel() === LogLevel.Trace) { + this.logService.trace('Relative path', extensionLocation.fsPath, this.extensionsLocation.fsPath, relativePath); + } + return relativePath; + } + return undefined; } private resolveExtensionLocation(path: string): URI { @@ -396,7 +402,7 @@ export abstract class AbstractExtensionsProfileScannerService extends Disposable function isStoredProfileExtension(candidate: any): candidate is IStoredProfileExtension { return isObject(candidate) && isIExtensionIdentifier(candidate.identifier) - && (isUriComponents(candidate.location) || isString(candidate.location)) + && (isUriComponents(candidate.location) || (isString(candidate.location) && candidate.location)) && (isUndefined(candidate.relativeLocation) || isString(candidate.relativeLocation)) && candidate.version && isString(candidate.version); } diff --git a/src/vs/platform/extensionManagement/node/extensionManagementService.ts b/src/vs/platform/extensionManagement/node/extensionManagementService.ts index 4147a0ccdae..0a3a830fcf4 100644 --- a/src/vs/platform/extensionManagement/node/extensionManagementService.ts +++ b/src/vs/platform/extensionManagement/node/extensionManagementService.ts @@ -164,13 +164,20 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi if (!local) { throw new Error(`Cannot find a valid extension from the location ${location.toString()}`); } - await this.addExtensionsToProfile([local], profileLocation); + await this.addExtensionsToProfile([[local, undefined]], profileLocation); this.logService.info('Successfully installed extension', local.identifier.id, profileLocation.toString()); return local; } - getMetadata(extension: ILocalExtension, profileLocation: URI = this.userDataProfilesService.defaultProfile.extensionsResource): Promise { - return this.extensionsScanner.scanMetadata(extension, profileLocation); + async installExtensionsFromProfile(extensions: IExtensionIdentifier[], fromProfileLocation: URI, toProfileLocation: URI): Promise { + this.logService.trace('ExtensionManagementService#installExtensionsFromProfile', extensions, fromProfileLocation.toString(), toProfileLocation.toString()); + const extensionsToInstall = (await this.extensionsScanner.scanExtensions(ExtensionType.User, fromProfileLocation)).filter(e => extensions.some(id => areSameExtensions(id, e.identifier))); + if (extensionsToInstall.length) { + const metadata = await Promise.all(extensionsToInstall.map(e => this.extensionsScanner.scanMetadata(e, fromProfileLocation))); + await this.addExtensionsToProfile(extensionsToInstall.map((e, index) => [e, metadata[index]]), toProfileLocation); + this.logService.info('Successfully installed extensions', extensionsToInstall.map(e => e.identifier.id), toProfileLocation.toString()); + } + return extensionsToInstall; } async updateMetadata(local: ILocalExtension, metadata: Partial, profileLocation: URI = this.userDataProfilesService.defaultProfile.extensionsResource): Promise { @@ -209,6 +216,10 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi return this.installFromGallery(galleryExtension); } + copyExtensions(fromProfileLocation: URI, toProfileLocation: URI): Promise { + return this.extensionsScanner.copyExtensions(fromProfileLocation, toProfileLocation); + } + markAsUninstalled(...extensions: IExtension[]): Promise { return this.extensionsScanner.setUninstalled(...extensions); } @@ -364,15 +375,16 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi } if (added.length) { - await this.addExtensionsToProfile(added, this.userDataProfilesService.defaultProfile.extensionsResource); + await this.addExtensionsToProfile(added.map(e => [e, undefined]), this.userDataProfilesService.defaultProfile.extensionsResource); this.logService.info('Added extensions to default profile from external source', added.map(e => e.identifier.id)); } } - private async addExtensionsToProfile(extensions: ILocalExtension[], profileLocation: URI): Promise { - await this.setInstalled(extensions); - await this.extensionsProfileScannerService.addExtensionsToProfile(extensions.map(local => ([local, undefined])), profileLocation); - this._onDidInstallExtensions.fire(extensions.map(local => ({ local, identifier: local.identifier, operation: InstallOperation.None, profileLocation }))); + private async addExtensionsToProfile(extensions: [ILocalExtension, Metadata | undefined][], profileLocation: URI): Promise { + const localExtensions = extensions.map(e => e[0]); + await this.setInstalled(localExtensions); + await this.extensionsProfileScannerService.addExtensionsToProfile(extensions, profileLocation); + this._onDidInstallExtensions.fire(localExtensions.map(local => ({ local, identifier: local.identifier, operation: InstallOperation.None, profileLocation }))); } private async setInstalled(extensions: ILocalExtension[]): Promise { @@ -530,6 +542,14 @@ export class ExtensionsScanner extends Disposable { await this.withUninstalledExtensions(uninstalled => delete uninstalled[ExtensionKey.create(extension).toString()]); } + async copyExtensions(fromProfileLocation: URI, toProfileLocation: URI): Promise { + const fromExtensions = await this.scanExtensions(ExtensionType.User, fromProfileLocation); + const extensions: [ILocalExtension, Metadata | undefined][] = await Promise.all(fromExtensions + .filter(e => !e.isApplicationScoped) /* remove application scoped extensions */ + .map(async e => ([e, await this.scanMetadata(e, fromProfileLocation)]))); + await this.extensionsProfileScannerService.addExtensionsToProfile(extensions, toProfileLocation); + } + private async withUninstalledExtensions(updateFn?: (uninstalled: IStringDictionary) => void): Promise> { return this.uninstalledFileLimiter.queue(async () => { let raw: string | undefined; diff --git a/src/vs/platform/extensionManagement/test/common/extensionsProfileScannerService.test.ts b/src/vs/platform/extensionManagement/test/common/extensionsProfileScannerService.test.ts index 0a86bed8233..579929a017f 100644 --- a/src/vs/platform/extensionManagement/test/common/extensionsProfileScannerService.test.ts +++ b/src/vs/platform/extensionManagement/test/common/extensionsProfileScannerService.test.ts @@ -414,7 +414,7 @@ suite('ExtensionsProfileScannerService', () => { test('read extension when manifest has empty lines and spaces', async () => { const extensionsManifest = joinPath(extensionsLocation, 'extensions.json'); - await instantiationService.get(IFileService).writeFile(extensionsManifest, VSBuffer.fromString(` + await instantiationService.get(IFileService).writeFile(extensionsManifest, VSBuffer.fromString(` `)); @@ -423,6 +423,25 @@ suite('ExtensionsProfileScannerService', () => { assert.deepStrictEqual(actual, []); }); + test('read extension when the relative location is empty', async () => { + const extensionsManifest = joinPath(extensionsLocation, 'extensions.json'); + const extension = aExtension('pub.a', joinPath(extensionsLocation, 'pub.a-1.0.0')); + await instantiationService.get(IFileService).writeFile(extensionsManifest, VSBuffer.fromString(JSON.stringify([{ + identifier: extension.identifier, + location: extension.location.toJSON(), + relativeLocation: '', + version: extension.manifest.version, + }]))); + + const testObject = instantiationService.createInstance(TestObject, extensionsLocation); + + const actual = await testObject.scanProfileExtensions(extensionsManifest); + assert.deepStrictEqual(actual.map(a => ({ ...a, location: a.location.toJSON() })), [{ identifier: extension.identifier, location: extension.location.toJSON(), version: extension.manifest.version, metadata: undefined }]); + + const manifestContent = JSON.parse((await instantiationService.get(IFileService).readFile(extensionsManifest)).value.toString()); + assert.deepStrictEqual(manifestContent, [{ identifier: extension.identifier, location: extension.location.toJSON(), relativeLocation: 'pub.a-1.0.0', version: extension.manifest.version }]); + }); + function aExtension(id: string, location: URI, e?: Partial): IExtension { return { identifier: { id }, diff --git a/src/vs/platform/extensions/common/extensions.ts b/src/vs/platform/extensions/common/extensions.ts index 959cf0c61a0..7ee47275468 100644 --- a/src/vs/platform/extensions/common/extensions.ts +++ b/src/vs/platform/extensions/common/extensions.ts @@ -409,7 +409,7 @@ export function isAuthenticationProviderExtension(manifest: IExtensionManifest): export function isResolverExtension(manifest: IExtensionManifest, remoteAuthority: string | undefined): boolean { if (remoteAuthority) { const activationEvent = `onResolveRemoteAuthority:${getRemoteName(remoteAuthority)}`; - return manifest.activationEvents?.indexOf(activationEvent) !== -1; + return !!manifest.activationEvents?.includes(activationEvent); } return false; } diff --git a/src/vs/platform/extensions/electron-main/extensionHostStarter.ts b/src/vs/platform/extensions/electron-main/extensionHostStarter.ts index 6f9be3f5ba1..fe838c2ba54 100644 --- a/src/vs/platform/extensions/electron-main/extensionHostStarter.ts +++ b/src/vs/platform/extensions/electron-main/extensionHostStarter.ts @@ -18,7 +18,7 @@ import { mixin } from 'vs/base/common/objects'; import * as platform from 'vs/base/common/platform'; import { cwd } from 'vs/base/common/process'; import { canUseUtilityProcess } from 'vs/base/parts/sandbox/electron-main/electronTypes'; -import { UtilityProcess } from 'vs/platform/utilityProcess/electron-main/utilityProcess'; +import { WindowUtilityProcess } from 'vs/platform/utilityProcess/electron-main/utilityProcess'; import { IWindowsMainService } from 'vs/platform/windows/electron-main/windows'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; @@ -27,7 +27,7 @@ export class ExtensionHostStarter implements IDisposable, IExtensionHostStarter private static _lastId: number = 0; - protected readonly _extHosts: Map; + protected readonly _extHosts: Map; private _shutdown = false; constructor( @@ -36,7 +36,7 @@ export class ExtensionHostStarter implements IDisposable, IExtensionHostStarter @IWindowsMainService private readonly _windowsMainService: IWindowsMainService, @ITelemetryService private readonly _telemetryService: ITelemetryService, ) { - this._extHosts = new Map(); + this._extHosts = new Map(); // On shutdown: gracefully await extension host shutdowns this._lifecycleMainService.onWillShutdown((e) => { @@ -49,7 +49,7 @@ export class ExtensionHostStarter implements IDisposable, IExtensionHostStarter // Intentionally not killing the extension host processes } - private _getExtHost(id: string): ExtensionHostProcess | UtilityProcess { + private _getExtHost(id: string): ExtensionHostProcess | WindowUtilityProcess { const extHostProcess = this._extHosts.get(id); if (!extHostProcess) { throw new Error(`Unknown extension host!`); @@ -71,7 +71,7 @@ export class ExtensionHostStarter implements IDisposable, IExtensionHostStarter onDynamicError(id: string): Event<{ error: SerializedError }> { const exthost = this._getExtHost(id); - if (exthost instanceof UtilityProcess) { + if (exthost instanceof WindowUtilityProcess) { return Event.None; } @@ -91,12 +91,12 @@ export class ExtensionHostStarter implements IDisposable, IExtensionHostStarter throw canceled(); } const id = String(++ExtensionHostStarter._lastId); - let extHost: UtilityProcess | ExtensionHostProcess; + let extHost: WindowUtilityProcess | ExtensionHostProcess; if (useUtilityProcess) { if (!canUseUtilityProcess) { throw new Error(`Cannot use UtilityProcess!`); } - extHost = new UtilityProcess(this._logService, this._windowsMainService, this._telemetryService, this._lifecycleMainService); + extHost = new WindowUtilityProcess(this._logService, this._windowsMainService, this._telemetryService, this._lifecycleMainService); } else { extHost = new ExtensionHostProcess(id, this._logService); } @@ -118,6 +118,7 @@ export class ExtensionHostStarter implements IDisposable, IExtensionHostStarter this._getExtHost(id).start({ ...opts, type: 'extensionHost', + entryPoint: 'vs/workbench/api/node/extensionHostProcess', args: ['--skipWorkspaceStorageLock'], execArgv: opts.execArgv, allowLoadingUnsignedLibraries: true, diff --git a/src/vs/platform/languagePacks/browser/languagePacks.ts b/src/vs/platform/languagePacks/browser/languagePacks.ts index f63b7b552fa..62dedb2242a 100644 --- a/src/vs/platform/languagePacks/browser/languagePacks.ts +++ b/src/vs/platform/languagePacks/browser/languagePacks.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import { CancellationTokenSource } from 'vs/base/common/cancellation'; -import { Language } from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; import { IExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IExtensionResourceLoaderService } from 'vs/platform/extensionResourceLoader/common/extensionResourceLoader'; @@ -20,7 +19,7 @@ export class WebLanguagePacksService extends LanguagePackBaseService { super(extensionGalleryService); } - async getBuiltInExtensionTranslationsUri(id: string): Promise { + async getBuiltInExtensionTranslationsUri(id: string, language: string): Promise { const queryTimeout = new CancellationTokenSource(); setTimeout(() => queryTimeout.cancel(), 1000); @@ -29,7 +28,7 @@ export class WebLanguagePacksService extends LanguagePackBaseService { let result; try { result = await this.extensionGalleryService.query({ - text: `tag:"lp-${Language.value()}"`, + text: `tag:"lp-${language}"`, pageSize: 5 }, queryTimeout.token); } catch (err) { @@ -39,7 +38,7 @@ export class WebLanguagePacksService extends LanguagePackBaseService { const languagePackExtensions = result.firstPage.find(e => e.properties.localizedLanguages?.length); if (!languagePackExtensions) { - this.logService.trace(`No language pack found for language ${Language.value()}`); + this.logService.trace(`No language pack found for language ${language}`); return undefined; } @@ -49,7 +48,7 @@ export class WebLanguagePacksService extends LanguagePackBaseService { const manifest = await this.extensionGalleryService.getManifest(languagePackExtensions, manifestTimeout.token); // Find the translation from the language pack - const localization = manifest?.contributes?.localizations?.find(l => l.languageId === Language.value()); + const localization = manifest?.contributes?.localizations?.find(l => l.languageId === language); const translation = localization?.translations.find(t => t.id === id); if (!translation) { this.logService.trace(`No translation found for id '${id}, in ${manifest?.name}`); diff --git a/src/vs/platform/languagePacks/common/languagePacks.ts b/src/vs/platform/languagePacks/common/languagePacks.ts index b17d660cb06..f682e997d8c 100644 --- a/src/vs/platform/languagePacks/common/languagePacks.ts +++ b/src/vs/platform/languagePacks/common/languagePacks.ts @@ -27,7 +27,7 @@ export interface ILanguagePackService { readonly _serviceBrand: undefined; getAvailableLanguages(): Promise>; getInstalledLanguages(): Promise>; - getBuiltInExtensionTranslationsUri(id: string): Promise; + getBuiltInExtensionTranslationsUri(id: string, language: string): Promise; } export abstract class LanguagePackBaseService extends Disposable implements ILanguagePackService { @@ -37,7 +37,7 @@ export abstract class LanguagePackBaseService extends Disposable implements ILan super(); } - abstract getBuiltInExtensionTranslationsUri(id: string): Promise; + abstract getBuiltInExtensionTranslationsUri(id: string, language: string): Promise; abstract getInstalledLanguages(): Promise>; diff --git a/src/vs/platform/languagePacks/node/languagePacks.ts b/src/vs/platform/languagePacks/node/languagePacks.ts index b88a3103146..0458bb1a712 100644 --- a/src/vs/platform/languagePacks/node/languagePacks.ts +++ b/src/vs/platform/languagePacks/node/languagePacks.ts @@ -16,7 +16,6 @@ import { areSameExtensions } from 'vs/platform/extensionManagement/common/extens import { ILogService } from 'vs/platform/log/common/log'; import { ILocalizationContribution } from 'vs/platform/extensions/common/extensions'; import { ILanguagePackItem, LanguagePackBaseService } from 'vs/platform/languagePacks/common/languagePacks'; -import { Language } from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; interface ILanguagePack { @@ -50,11 +49,11 @@ export class NativeLanguagePackService extends LanguagePackBaseService { }); } - async getBuiltInExtensionTranslationsUri(id: string): Promise { + async getBuiltInExtensionTranslationsUri(id: string, language: string): Promise { const packs = await this.cache.getLanguagePacks(); - const pack = packs[Language.value()]; + const pack = packs[language]; if (!pack) { - this.logService.warn(`No language pack found for ${Language.value()}`); + this.logService.warn(`No language pack found for ${language}`); return undefined; } diff --git a/src/vs/platform/lifecycle/electron-browser/sharedProcessLifecycleService.ts b/src/vs/platform/lifecycle/electron-browser/sharedProcessLifecycleService.ts index a23e4b912a6..dfe975ec19f 100644 --- a/src/vs/platform/lifecycle/electron-browser/sharedProcessLifecycleService.ts +++ b/src/vs/platform/lifecycle/electron-browser/sharedProcessLifecycleService.ts @@ -8,7 +8,7 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; -export const ISharedProcessLifecycleService = createDecorator('lifecycleSharedProcessService'); +export const ISharedProcessLifecycleService = createDecorator('sharedProcessLifecycleService'); export interface ISharedProcessLifecycleService { readonly _serviceBrand: undefined; diff --git a/src/vs/platform/policy/node/nativePolicyService.ts b/src/vs/platform/policy/node/nativePolicyService.ts index 7b78806548f..eae3bf8dfac 100644 --- a/src/vs/platform/policy/node/nativePolicyService.ts +++ b/src/vs/platform/policy/node/nativePolicyService.ts @@ -6,7 +6,7 @@ import { AbstractPolicyService, IPolicyService, PolicyDefinition } from 'vs/platform/policy/common/policy'; import { IStringDictionary } from 'vs/base/common/collections'; import { Throttler } from 'vs/base/common/async'; -import { createWatcher, PolicyUpdate, Watcher } from 'vscode-policy-watcher'; +import { createWatcher, PolicyUpdate, Watcher } from '@vscode/policy-watcher'; import { MutableDisposable } from 'vs/base/common/lifecycle'; import { ILogService } from 'vs/platform/log/common/log'; diff --git a/src/vs/platform/quickinput/browser/quickInput.ts b/src/vs/platform/quickinput/browser/quickInput.ts index 798f6ccb2f6..8c1786f8654 100644 --- a/src/vs/platform/quickinput/browser/quickInput.ts +++ b/src/vs/platform/quickinput/browser/quickInput.ts @@ -9,6 +9,7 @@ import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; import { Button, IButtonStyles } from 'vs/base/browser/ui/button/button'; import { CountBadge, ICountBadgeStyles } from 'vs/base/browser/ui/countBadge/countBadge'; +import { IHoverDelegate } from 'vs/base/browser/ui/iconLabel/iconHoverDelegate'; import { IInputBoxStyles } from 'vs/base/browser/ui/inputbox/inputBox'; import { IKeybindingLabelStyles } from 'vs/base/browser/ui/keybindingLabel/keybindingLabel'; import { IListRenderer, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; @@ -51,6 +52,7 @@ export interface IQuickInputOptions { renderers: IListRenderer[], options: IListOptions, ): List; + hoverDelegate: IHoverDelegate; styles: IQuickInputStyles; } @@ -1416,6 +1418,12 @@ export class QuickInputController extends Disposable { } } break; + case KeyCode.Space: + if (event.ctrlKey) { + dom.EventHelper.stop(e, true); + this.getUI().list.toggleHover(); + } + break; } })); diff --git a/src/vs/platform/quickinput/browser/quickInputList.ts b/src/vs/platform/quickinput/browser/quickInputList.ts index 30aea174bf6..650b9aa65ee 100644 --- a/src/vs/platform/quickinput/browser/quickInputList.ts +++ b/src/vs/platform/quickinput/browser/quickInputList.ts @@ -7,19 +7,23 @@ import * as dom from 'vs/base/browser/dom'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { AriaRole } from 'vs/base/browser/ui/aria/aria'; +import { HoverPosition } from 'vs/base/browser/ui/hover/hoverWidget'; +import { IHoverWidget } from 'vs/base/browser/ui/iconLabel/iconHoverDelegate'; import { IconLabel, IIconLabelValueOptions } from 'vs/base/browser/ui/iconLabel/iconLabel'; import { KeybindingLabel } from 'vs/base/browser/ui/keybindingLabel/keybindingLabel'; import { IListRenderer, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { IListAccessibilityProvider, IListOptions, IListStyles, List } from 'vs/base/browser/ui/list/listWidget'; import { IAction } from 'vs/base/common/actions'; import { range } from 'vs/base/common/arrays'; +import { ThrottledDelayer } from 'vs/base/common/async'; import { compareAnything } from 'vs/base/common/comparers'; import { memoize } from 'vs/base/common/decorators'; import { Emitter, Event } from 'vs/base/common/event'; import { IMatch } from 'vs/base/common/filters'; +import { IMarkdownString } from 'vs/base/common/htmlContent'; import { getCodiconAriaLabel, IParsedLabelWithIcons, matchesFuzzyIconAware, parseLabelWithIcons } from 'vs/base/common/iconLabels'; import { KeyCode } from 'vs/base/common/keyCodes'; -import { dispose, IDisposable } from 'vs/base/common/lifecycle'; +import { DisposableStore, dispose, IDisposable } from 'vs/base/common/lifecycle'; import * as platform from 'vs/base/common/platform'; import { ltrim } from 'vs/base/common/strings'; import { withNullAsUndefined } from 'vs/base/common/types'; @@ -41,6 +45,7 @@ interface IListElement { readonly saneAriaLabel: string; readonly saneDescription?: string; readonly saneDetail?: string; + readonly saneTooltip?: string | IMarkdownString | HTMLElement; readonly labelHighlights?: IMatch[]; readonly descriptionHighlights?: IMatch[]; readonly detailHighlights?: IMatch[]; @@ -60,6 +65,8 @@ class ListElement implements IListElement, IDisposable { saneAriaLabel!: string; saneDescription?: string; saneDetail?: string; + saneTooltip?: string | IMarkdownString | HTMLElement; + element?: HTMLElement; hidden = false; private readonly _onChecked = new Emitter(); onChecked = this._onChecked.event; @@ -159,6 +166,7 @@ class ListElementRenderer implements IListRenderer { + // If we hover over an anchor element, we don't want to show the hover because + // the anchor may have a tooltip that we want to show instead. + if (e.browserEvent.target instanceof HTMLAnchorElement) { + delayer.cancel(); + return; + } + if ( + // anchors are an exception as called out above so we skip them here + !(e.browserEvent.relatedTarget instanceof HTMLAnchorElement) && + // check if the mouse is still over the same element + dom.isAncestor(e.browserEvent.relatedTarget as Node, e.element?.element as Node) + ) { + return; + } + await delayer.trigger(async () => { + if (e.element) { + this.showHover(e.element); + } + }); + })); + this.disposables.push(this.list.onMouseOut(e => { + // onMouseOut triggers every time a new element has been moused over + // even if it's on the same list item. We only want one event, so we + // check if the mouse is still over the same element. + if (dom.isAncestor(e.browserEvent.relatedTarget as Node, e.element?.element as Node)) { + return; + } + delayer.cancel(); + })); this.disposables.push( this._onChangedAllVisibleChecked, this._onChangedCheckedCount, @@ -389,7 +433,8 @@ export class QuickInputList { this._onButtonTriggered, this._onSeparatorButtonTriggered, this._onLeave, - this._onKeyDown + this._onKeyDown, + delayer ); } @@ -475,7 +520,7 @@ export class QuickInputList { const saneLabel = item.label ? item.label.replace(/\r?\n/g, ' ') : ''; const saneSortLabel = parseLabelWithIcons(saneLabel).text.trim(); - let saneMeta, saneDescription, saneDetail, labelHighlights, descriptionHighlights, detailHighlights; + let saneMeta, saneDescription, saneDetail, labelHighlights, descriptionHighlights, detailHighlights, saneTooltip; if (item.type !== 'separator') { saneMeta = item.meta && item.meta.replace(/\r?\n/g, ' '); saneDescription = item.description && item.description.replace(/\r?\n/g, ' '); @@ -483,6 +528,7 @@ export class QuickInputList { labelHighlights = item.highlights?.label; descriptionHighlights = item.highlights?.description; detailHighlights = item.highlights?.detail; + saneTooltip = item.tooltip; } const saneAriaLabel = item.ariaLabel || [saneLabel, saneDescription, saneDetail] .map(s => getCodiconAriaLabel(s)) @@ -512,6 +558,7 @@ export class QuickInputList { saneAriaLabel, saneDescription, saneDetail, + saneTooltip, labelHighlights, descriptionHighlights, detailHighlights, @@ -659,6 +706,30 @@ export class QuickInputList { this.list.domFocus(); } + /** + * Disposes of the hover and shows a new one for the given index if it has a tooltip. + * @param element The element to show the hover for + */ + private showHover(element: ListElement): void { + if (this._lastHover && !this._lastHover.isDisposed) { + this.options.hoverDelegate.onDidHideHover?.(); + this._lastHover?.dispose(); + } + if (!element.element || !element.saneTooltip) { + return; + } + this._lastHover = this.options.hoverDelegate.showHover({ + content: element.saneTooltip!, + target: element.element!, + linkHandler: (url) => { + this.options.linkOpenerDelegate(url); + }, + showPointer: true, + container: this.container, + hoverPosition: HoverPosition.RIGHT + }, false); + } + layout(maxHeight?: number): void { this.list.getHTMLElement().style.maxHeight = maxHeight ? `calc(${Math.floor(maxHeight / 44) * 44}px)` : ''; this.list.layout(); @@ -795,6 +866,37 @@ export class QuickInputList { style(styles: IListStyles) { this.list.style(styles); } + + toggleHover() { + const element = this.list.getFocusedElements()[0]; + if (!element.saneTooltip) { + return; + } + + // if there's a hover already, hide it (toggle off) + if (this._lastHover && !this._lastHover.isDisposed) { + this._lastHover.dispose(); + return; + } + + // If there is no hover, show it (toggle on) + const focused = this.list.getFocusedElements()[0]; + if (!focused) { + return; + } + this.showHover(focused); + const store = new DisposableStore(); + store.add(this.list.onDidChangeFocus(e => { + if (e.indexes.length) { + this.showHover(e.elements[0]); + } + })); + if (this._lastHover) { + store.add(this._lastHover); + } + this._toggleHover = store; + this.elementDisposables.push(this._toggleHover); + } } function matchesContiguousIconAware(query: string, target: IParsedLabelWithIcons): IMatch[] | null { diff --git a/src/vs/platform/quickinput/browser/quickInputService.ts b/src/vs/platform/quickinput/browser/quickInputService.ts index 90b8ed4ef38..5e7473475d7 100644 --- a/src/vs/platform/quickinput/browser/quickInputService.ts +++ b/src/vs/platform/quickinput/browser/quickInputService.ts @@ -89,6 +89,12 @@ export class QuickInputService extends Themable implements IQuickInputService { renderers: IListRenderer[], options: IWorkbenchListOptions ) => this.instantiationService.createInstance(WorkbenchList, user, container, delegate, renderers, options) as List, + hoverDelegate: { + showHover(options, focus) { + return undefined; + }, + delay: 200 + }, styles: this.computeStyles() }; diff --git a/src/vs/platform/quickinput/common/quickInput.ts b/src/vs/platform/quickinput/common/quickInput.ts index ff260f92f56..7e3bd52dd2d 100644 --- a/src/vs/platform/quickinput/common/quickInput.ts +++ b/src/vs/platform/quickinput/common/quickInput.ts @@ -14,6 +14,7 @@ import { IDisposable } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; import Severity from 'vs/base/common/severity'; import { URI } from 'vs/base/common/uri'; +import { IMarkdownString } from 'vs/base/common/htmlContent'; export interface IQuickPickItemHighlights { label?: IMatch[]; @@ -31,6 +32,7 @@ export interface IQuickPickItem { ariaLabel?: string; description?: string; detail?: string; + tooltip?: string | IMarkdownString; /** * Allows to show a keybinding next to the item to indicate * how the item can be triggered outside of the picker using @@ -52,6 +54,7 @@ export interface IQuickPickSeparator { label?: string; ariaLabel?: string; buttons?: readonly IQuickInputButton[]; + tooltip?: string | IMarkdownString; } export interface IKeyMods { diff --git a/src/vs/platform/quickinput/test/browser/quickinput.test.ts b/src/vs/platform/quickinput/test/browser/quickinput.test.ts index 5e74d3548bf..34a0547388b 100644 --- a/src/vs/platform/quickinput/test/browser/quickinput.test.ts +++ b/src/vs/platform/quickinput/test/browser/quickinput.test.ts @@ -58,6 +58,12 @@ suite('QuickInput', () => { // https://github.com/microsoft/vscode/issues/147543 renderers: IListRenderer[], options: IListOptions, ) => new List(user, container, delegate, renderers, options), + hoverDelegate: { + showHover(options, focus) { + return undefined; + }, + delay: 200 + }, styles: { button: unthemedButtonStyles, countBadge: unthemedCountStyles, diff --git a/src/vs/platform/remote/common/remoteAgentConnection.ts b/src/vs/platform/remote/common/remoteAgentConnection.ts index 8cbc8ec31c4..bbbddcc39d5 100644 --- a/src/vs/platform/remote/common/remoteAgentConnection.ts +++ b/src/vs/platform/remote/common/remoteAgentConnection.ts @@ -583,7 +583,8 @@ export abstract class PersistentConnection extends Disposable { return this._permanentFailure || PersistentConnection._permanentFailure; } - private _isReconnecting: boolean; + private _isReconnecting: boolean = false; + private _isDisposed: boolean = false; constructor( private readonly _connectionType: ConnectionType, @@ -593,7 +594,6 @@ export abstract class PersistentConnection extends Disposable { private readonly _reconnectionFailureIsFatal: boolean ) { super(); - this._isReconnecting = false; this._onDidStateChange.fire(new ConnectionGainEvent(this.reconnectionToken, 0, 0)); @@ -640,6 +640,11 @@ export abstract class PersistentConnection extends Disposable { } } + public override dispose(): void { + super.dispose(); + this._isDisposed = true; + } + private async _beginReconnecting(): Promise { // Only have one reconnection loop active at a time. if (this._isReconnecting) { @@ -654,7 +659,7 @@ export abstract class PersistentConnection extends Disposable { } private async _runReconnectingLoop(): Promise { - if (this._isPermanentFailure) { + if (this._isPermanentFailure || this._isDisposed) { // no more attempts! return; } @@ -735,7 +740,7 @@ export abstract class PersistentConnection extends Disposable { this._onReconnectionPermanentFailure(this.protocol.getMillisSinceLastIncomingData(), attempt + 1, false); break; } - } while (!this._isPermanentFailure); + } while (!this._isPermanentFailure && !this._isDisposed); } private _onReconnectionPermanentFailure(millisSinceLastIncomingData: number, attempt: number, handled: boolean): void { diff --git a/src/vs/platform/remote/common/remoteAgentEnvironment.ts b/src/vs/platform/remote/common/remoteAgentEnvironment.ts index f850e0d5216..d83efeb9408 100644 --- a/src/vs/platform/remote/common/remoteAgentEnvironment.ts +++ b/src/vs/platform/remote/common/remoteAgentEnvironment.ts @@ -6,6 +6,7 @@ import * as performance from 'vs/base/common/performance'; import { OperatingSystem } from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; +import { IUserDataProfile } from 'vs/platform/userDataProfile/common/userDataProfile'; export interface IRemoteAgentEnvironment { pid: number; @@ -22,6 +23,10 @@ export interface IRemoteAgentEnvironment { arch: string; marks: performance.PerformanceMark[]; useHostProxy: boolean; + profiles: { + all: IUserDataProfile[]; + home: URI; + }; } export interface RemoteAgentConnectionContext { diff --git a/src/vs/platform/remote/common/remoteExtensionsScanner.ts b/src/vs/platform/remote/common/remoteExtensionsScanner.ts new file mode 100644 index 00000000000..792c0352bb7 --- /dev/null +++ b/src/vs/platform/remote/common/remoteExtensionsScanner.ts @@ -0,0 +1,20 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { URI } from 'vs/base/common/uri'; +import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; + +export const IRemoteExtensionsScannerService = createDecorator('IRemoteExtensionsScannerService'); + +export const RemoteExtensionsScannerChannelName = 'remoteExtensionsScanner'; + +export interface IRemoteExtensionsScannerService { + readonly _serviceBrand: undefined; + + whenExtensionsReady(): Promise; + scanExtensions(): Promise; + scanSingleExtension(extensionLocation: URI, isBuiltin: boolean): Promise; +} diff --git a/src/vs/platform/request/electron-browser/sharedProcessRequestService.ts b/src/vs/platform/request/electron-browser/sharedProcessRequestService.ts index 9af32dd592d..4a286e4eaf2 100644 --- a/src/vs/platform/request/electron-browser/sharedProcessRequestService.ts +++ b/src/vs/platform/request/electron-browser/sharedProcessRequestService.ts @@ -9,7 +9,6 @@ import { IRequestContext, IRequestOptions } from 'vs/base/parts/request/common/r import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IMainProcessService } from 'vs/platform/ipc/electron-sandbox/services'; import { ILogService } from 'vs/platform/log/common/log'; -import { IProductService } from 'vs/platform/product/common/productService'; import { RequestService } from 'vs/platform/request/browser/requestService'; import { IRequestService } from 'vs/platform/request/common/request'; import { RequestChannelClient } from 'vs/platform/request/common/requestIpc'; @@ -24,7 +23,6 @@ export class SharedProcessRequestService implements IRequestService { constructor( mainProcessService: IMainProcessService, private readonly configurationService: IConfigurationService, - private readonly productService: IProductService, private readonly logService: ILogService, ) { this.browserRequestService = new RequestService(configurationService, logService); @@ -53,6 +51,6 @@ export class SharedProcessRequestService implements IRequestService { if (isBoolean(value)) { return value; } - return this.productService.quality !== 'stable'; + return true; } } diff --git a/src/vs/platform/severityIcon/browser/media/severityIcon.css b/src/vs/platform/severityIcon/browser/media/severityIcon.css index 06681b4c2b8..4fd1d06c80b 100644 --- a/src/vs/platform/severityIcon/browser/media/severityIcon.css +++ b/src/vs/platform/severityIcon/browser/media/severityIcon.css @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ .monaco-editor .zone-widget .codicon.codicon-error, -.markers-panel .marker-icon.codicon.codicon-error, +.markers-panel .marker-icon.error, .markers-panel .marker-icon .codicon.codicon-error, .text-search-provider-messages .providerMessage .codicon.codicon-error, .extensions-viewlet > .extensions .codicon.codicon-error, .extension-editor .codicon.codicon-error, @@ -13,7 +13,7 @@ } .monaco-editor .zone-widget .codicon.codicon-warning, -.markers-panel .marker-icon.codicon.codicon-warning, +.markers-panel .marker-icon.warning, .markers-panel .marker-icon .codicon.codicon-warning, .text-search-provider-messages .providerMessage .codicon.codicon-warning, .extensions-viewlet > .extensions .codicon.codicon-warning, .extension-editor .codicon.codicon-warning, @@ -22,7 +22,7 @@ } .monaco-editor .zone-widget .codicon.codicon-info, -.markers-panel .marker-icon.codicon.codicon-info, +.markers-panel .marker-icon.info, .markers-panel .marker-icon .codicon.codicon-info, .text-search-provider-messages .providerMessage .codicon.codicon-info, .extensions-viewlet > .extensions .codicon.codicon-info, .extension-editor .codicon.codicon-info, diff --git a/src/vs/platform/sharedProcess/electron-main/sharedProcess.ts b/src/vs/platform/sharedProcess/electron-main/sharedProcess.ts index 46fca623948..0fc12536e0c 100644 --- a/src/vs/platform/sharedProcess/electron-main/sharedProcess.ts +++ b/src/vs/platform/sharedProcess/electron-main/sharedProcess.ts @@ -5,7 +5,7 @@ import { BrowserWindow, Event as ElectronEvent, IpcMainEvent, MessagePortMain } from 'electron'; import { validatedIpcMain } from 'vs/base/parts/ipc/electron-main/ipcMain'; -import { Barrier } from 'vs/base/common/async'; +import { Barrier, DeferredPromise } from 'vs/base/common/async'; import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { FileAccess } from 'vs/base/common/network'; @@ -25,16 +25,24 @@ import { toErrorMessage } from 'vs/base/common/errorMessage'; import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; import { IPolicyService } from 'vs/platform/policy/common/policy'; import { ILoggerMainService } from 'vs/platform/log/electron-main/loggerService'; +import { UtilityProcess } from 'vs/platform/utilityProcess/electron-main/utilityProcess'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; +import { canUseUtilityProcess } from 'vs/base/parts/sandbox/electron-main/electronTypes'; +import { parseSharedProcessDebugPort } from 'vs/platform/environment/node/environmentService'; export class SharedProcess extends Disposable implements ISharedProcess { + private readonly _onDidError = this._register(new Emitter<{ type: WindowError; details?: { reason: string; exitCode: number } }>()); + readonly onDidError = Event.buffer(this._onDidError.event); // buffer until we have a listener! + private readonly firstWindowConnectionBarrier = new Barrier(); private window: BrowserWindow | undefined = undefined; private windowCloseListener: ((event: ElectronEvent) => void) | undefined = undefined; - private readonly _onDidError = this._register(new Emitter<{ type: WindowError; details?: { reason: string; exitCode: number } }>()); - readonly onDidError = Event.buffer(this._onDidError.event); // buffer until we have a listener! + private utilityProcess: UtilityProcess | undefined = undefined; + private readonly useUtilityProcess = canUseUtilityProcess && this.configurationService.getValue('window.experimental.sharedProcessUseUtilityProcess'); constructor( private readonly machineId: string, @@ -46,11 +54,16 @@ export class SharedProcess extends Disposable implements ISharedProcess { @ILoggerMainService private readonly loggerMainService: ILoggerMainService, @IPolicyService private readonly policyService: IPolicyService, @IThemeMainService private readonly themeMainService: IThemeMainService, - @IProtocolMainService private readonly protocolMainService: IProtocolMainService + @IProtocolMainService private readonly protocolMainService: IProtocolMainService, + @IConfigurationService private readonly configurationService: IConfigurationService ) { super(); this.registerListeners(); + + if (this.useUtilityProcess) { + this.logService.info('[SharedProcess] using utility process'); + } } private registerListeners(): void { @@ -66,7 +79,7 @@ export class SharedProcess extends Disposable implements ISharedProcess { } private async onWindowConnection(e: IpcMainEvent, nonce: string): Promise { - this.logService.trace('SharedProcess: on vscode:createSharedProcessMessageChannel'); + this.logService.trace('[SharedProcess] on vscode:createSharedProcessMessageChannel'); // release barrier if this is the first window connection if (!this.firstWindowConnectionBarrier.isOpen()) { @@ -76,15 +89,17 @@ export class SharedProcess extends Disposable implements ISharedProcess { // await the shared process to be overall ready // we do not just wait for IPC ready because the // workbench window will communicate directly + await this.whenReady(); - // connect to the shared process window + // connect to the shared process const port = await this.connect(); // Check back if the requesting window meanwhile closed // Since shared process is delayed on startup there is // a chance that the window close before the shared process // was ready for a connection. + if (e.sender.isDestroyed()) { return port.close(); } @@ -94,22 +109,22 @@ export class SharedProcess extends Disposable implements ISharedProcess { } private onWorkerConnection(e: IpcMainEvent, configuration: IUtilityProcessWorkerConfiguration): void { - this.logService.trace('SharedProcess: onWorkerConnection', configuration); + this.logService.trace('[SharedProcess] onWorkerConnection', configuration); const disposables = new DisposableStore(); const disposeWorker = (reason: string) => { - if (!this.isAlive()) { + if (!this.isWindowAlive()) { return; // the shared process is already gone, no need to dispose anything } - this.logService.trace(`SharedProcess: disposing worker (reason: '${reason}')`, configuration); + this.logService.trace(`[SharedProcess] disposing worker (reason: '${reason}')`, configuration); // Only once! disposables.dispose(); // Send this into the shared process who owns workers - this.send('vscode:electron-main->shared-process=disposeWorker', configuration); + this.sendToWindow('vscode:electron-main->shared-process=disposeWorker', configuration); }; // Ensure the sender is a valid target to send to @@ -131,43 +146,49 @@ export class SharedProcess extends Disposable implements ISharedProcess { } private onWillShutdown(): void { - this.logService.trace('SharedProcess: onWillShutdown'); + this.logService.trace('[SharedProcess] onWillShutdown'); - const window = this.window; - if (!window) { - return; // possibly too early before created - } + if (this.utilityProcess) { + this.utilityProcess.postMessage('vscode:electron-main->shared-process=exit'); - // Signal exit to shared process when shutting down - this.send('vscode:electron-main->shared-process=exit'); - - // Shut the shared process down when we are quitting - // - // Note: because we veto the window close, we must first remove our veto. - // Otherwise the application would never quit because the shared process - // window is refusing to close! - // - if (this.windowCloseListener) { - window.removeListener('close', this.windowCloseListener); - this.windowCloseListener = undefined; - } - - // Electron seems to crash on Windows without this setTimeout :| - setTimeout(() => { - try { - this.logService.trace('SharedProcess: onWillShutdown window.close()'); - window.close(); - } catch (error) { - this.logService.trace(`SharedProcess: onWillShutdown window.close() error: ${error}`); // ignore, as electron is already shutting down + this.utilityProcess = undefined; + } else { + const window = this.window; + if (!window) { + return; // possibly too early before created } - this.window = undefined; - }, 0); + // Signal exit to shared process when shutting down + this.sendToWindow('vscode:electron-main->shared-process=exit'); + + // Shut the shared process down when we are quitting + // + // Note: because we veto the window close, we must first remove our veto. + // Otherwise the application would never quit because the shared process + // window is refusing to close! + // + if (this.windowCloseListener) { + window.removeListener('close', this.windowCloseListener); + this.windowCloseListener = undefined; + } + + // Electron seems to crash on Windows without this setTimeout :| + setTimeout(() => { + try { + this.logService.trace('[SharedProcess] onWillShutdown window.close()'); + window.close(); + } catch (error) { + this.logService.trace(`[SharedProcess] onWillShutdown window.close() error: ${error}`); // ignore, as electron is already shutting down + } + + this.window = undefined; + }, 0); + } } - private send(channel: string, ...args: any[]): void { - if (!this.isAlive()) { - this.logService.warn(`Sending IPC message to channel '${channel}' for shared process window that is destroyed`); + private sendToWindow(channel: string, ...args: any[]): void { + if (!this.isWindowAlive()) { + this.logService.warn(`Sending IPC message to channel '${channel}' for shared process that is destroyed`); return; } @@ -181,13 +202,24 @@ export class SharedProcess extends Disposable implements ISharedProcess { private _whenReady: Promise | undefined = undefined; whenReady(): Promise { if (!this._whenReady) { - // Overall signal that the shared process window was loaded and - // all services within have been created. - this._whenReady = new Promise(resolve => validatedIpcMain.once('vscode:shared-process->electron-main=init-done', () => { - this.logService.trace('SharedProcess: Overall ready'); + this._whenReady = (async () => { - resolve(); - })); + // Wait for shared process being ready to accept connection + await this.whenIpcReady; + + // Overall signal that the shared process was loaded and + // all services within have been created. + + const whenReady = new DeferredPromise(); + if (this.utilityProcess) { + this.utilityProcess.once('vscode:shared-process->electron-main=init-done', () => whenReady.complete()); + } else { + validatedIpcMain.once('vscode:shared-process->electron-main=init-done', () => whenReady.complete()); + } + + await whenReady.p; + this.logService.trace('[SharedProcess] Overall ready'); + })(); } return this._whenReady; @@ -201,24 +233,60 @@ export class SharedProcess extends Disposable implements ISharedProcess { // Always wait for first window asking for connection await this.firstWindowConnectionBarrier.wait(); - // Create window for shared process - this.createWindow(); + // Spawn shared process + this.spawn(); - // Listeners - this.registerWindowListeners(); + // Wait for shared process indicating that IPC connections are accepted + const sharedProcessIpcReady = new DeferredPromise(); + if (this.utilityProcess) { + this.utilityProcess.once('vscode:shared-process->electron-main=ipc-ready', () => sharedProcessIpcReady.complete()); + } else { + validatedIpcMain.once('vscode:shared-process->electron-main=ipc-ready', () => sharedProcessIpcReady.complete()); + } - // Wait for window indicating that IPC connections are accepted - await new Promise(resolve => validatedIpcMain.once('vscode:shared-process->electron-main=ipc-ready', () => { - this.logService.trace('SharedProcess: IPC ready'); - - resolve(); - })); + await sharedProcessIpcReady.p; + this.logService.trace('[SharedProcess] IPC ready'); })(); } return this._whenIpcReady; } + private spawn(): void { + + // Spawn shared process + if (this.useUtilityProcess) { + this.createUtilityProcess(); + } else { + this.createWindow(); + } + + // Listeners + this.registerSharedProcessListeners(); + } + + private createUtilityProcess(): void { + this.utilityProcess = this._register(new UtilityProcess(this.logService, NullTelemetryService, this.lifecycleMainService)); + + const inspectParams = parseSharedProcessDebugPort(this.environmentMainService.args, this.environmentMainService.isBuilt); + let execArgv: string[] | undefined = undefined; + if (inspectParams.port) { + execArgv = ['--nolazy']; + if (inspectParams.break) { + execArgv.push(`--inspect-brk=${inspectParams.port}`); + } else { + execArgv.push(`--inspect=${inspectParams.port}`); + } + } + + this.utilityProcess.start({ + type: 'shared-process', + entryPoint: 'vs/code/node/sharedProcess/sharedProcessMain', + payload: this.createSharedProcessConfiguration(), + execArgv + }); + } + private createWindow(): void { const configObjectUrl = this._register(this.protocolMainService.createIPCObjectUrl()); @@ -241,50 +309,61 @@ export class SharedProcess extends Disposable implements ISharedProcess { }); // Store into config object URL - configObjectUrl.update({ + configObjectUrl.update(this.createSharedProcessConfiguration()); + + // Load with config + this.window.loadURL(FileAccess.asBrowserUri(`vs/code/node/sharedProcess/sharedProcess${this.environmentMainService.isBuilt ? '' : '-dev'}.html`).toString(true)); + } + + private createSharedProcessConfiguration(): ISharedProcessConfiguration { + return { machineId: this.machineId, - windowId: this.window.id, + windowId: this.window?.id ?? -1, // TODO@bpasero what window id should this be in utility process? appRoot: this.environmentMainService.appRoot, codeCachePath: this.environmentMainService.codeCachePath, - profiles: this.userDataProfilesService.profiles, + profiles: { + home: this.userDataProfilesService.profilesHome, + all: this.userDataProfilesService.profiles, + }, userEnv: this.userEnv, args: this.environmentMainService.args, logLevel: this.loggerMainService.getLogLevel(), loggers: this.loggerMainService.getRegisteredLoggers(), product, policiesData: this.policyService.serialize() - }); - - // Load with config - this.window.loadURL(FileAccess.asBrowserUri(`vs/code/electron-browser/sharedProcess/sharedProcess${this.environmentMainService.isBuilt ? '' : '-dev'}.html`).toString(true)); + }; } - private registerWindowListeners(): void { - if (!this.window) { - return; + private registerSharedProcessListeners(): void { + + // Hidden window + if (this.window) { + + // Prevent the window from closing + this.windowCloseListener = (e: ElectronEvent) => { + this.logService.trace('SharedProcess#close prevented'); + + // We never allow to close the shared process unless we get explicitly disposed() + e.preventDefault(); + + // Still hide the window though if visible + if (this.window?.isVisible()) { + this.window.hide(); + } + }; + + this.window.on('close', this.windowCloseListener); + + // Crashes & Unresponsive & Failed to load + this.window.webContents.on('render-process-gone', (event, details) => this._onDidError.fire({ type: WindowError.PROCESS_GONE, details })); + this.window.on('unresponsive', () => this._onDidError.fire({ type: WindowError.UNRESPONSIVE })); + this.window.webContents.on('did-fail-load', (event, exitCode, reason) => this._onDidError.fire({ type: WindowError.LOAD, details: { reason, exitCode } })); } - // Prevent the window from closing - this.windowCloseListener = (e: ElectronEvent) => { - this.logService.trace('SharedProcess#close prevented'); - - // We never allow to close the shared process unless we get explicitly disposed() - e.preventDefault(); - - // Still hide the window though if visible - if (this.window?.isVisible()) { - this.window.hide(); - } - }; - - this.window.on('close', this.windowCloseListener); - - // Crashes & Unresponsive & Failed to load - // We use `onUnexpectedError` explicitly because the error handler - // will send the error to the active window to log in devtools too - this.window.webContents.on('render-process-gone', (event, details) => this._onDidError.fire({ type: WindowError.PROCESS_GONE, details })); - this.window.on('unresponsive', () => this._onDidError.fire({ type: WindowError.UNRESPONSIVE })); - this.window.webContents.on('did-fail-load', (event, exitCode, reason) => this._onDidError.fire({ type: WindowError.LOAD, details: { reason, exitCode } })); + // Utility process + else if (this.utilityProcess) { + this._register(this.utilityProcess.onCrash(event => this._onDidError.fire({ type: WindowError.PROCESS_GONE, details: { reason: event.reason, exitCode: event.code } }))); + } } async connect(): Promise { @@ -293,8 +372,11 @@ export class SharedProcess extends Disposable implements ISharedProcess { await this.whenIpcReady; // Connect and return message port - const window = assertIsDefined(this.window); - return connectMessagePort(window); + if (this.utilityProcess) { + return this.utilityProcess.connect(); + } else { + return connectMessagePort(assertIsDefined(this.window)); + } } async toggle(): Promise { @@ -319,7 +401,11 @@ export class SharedProcess extends Disposable implements ISharedProcess { return this.window?.isVisible() ?? false; } - private isAlive(): boolean { + usingUtilityProcess(): boolean { + return !!this.utilityProcess; + } + + private isWindowAlive(): boolean { const window = this.window; if (!window) { return false; diff --git a/src/vs/platform/sharedProcess/node/sharedProcess.ts b/src/vs/platform/sharedProcess/node/sharedProcess.ts index 3b568b3b57f..d01f16c6a96 100644 --- a/src/vs/platform/sharedProcess/node/sharedProcess.ts +++ b/src/vs/platform/sharedProcess/node/sharedProcess.ts @@ -9,7 +9,7 @@ import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; import { ILoggerResource, LogLevel } from 'vs/platform/log/common/log'; import { IUserDataProfile } from 'vs/platform/userDataProfile/common/userDataProfile'; import { PolicyDefinition, PolicyValue } from 'vs/platform/policy/common/policy'; -import { UriDto } from 'vs/base/common/uri'; +import { UriComponents, UriDto } from 'vs/base/common/uri'; export interface ISharedProcess { @@ -29,7 +29,10 @@ export interface ISharedProcessConfiguration extends ISandboxConfiguration { readonly loggers: UriDto[]; - readonly profiles: readonly UriDto[]; + readonly profiles: { + readonly home: UriComponents; + readonly all: readonly UriDto[]; + }; readonly policiesData?: IStringDictionary<{ definition: PolicyDefinition; value: PolicyValue }>; } diff --git a/src/vs/platform/terminal/common/terminal.ts b/src/vs/platform/terminal/common/terminal.ts index dd90c56776c..4877f38c2b7 100644 --- a/src/vs/platform/terminal/common/terminal.ts +++ b/src/vs/platform/terminal/common/terminal.ts @@ -84,6 +84,7 @@ export const enum TerminalSettingId { CommandsToSkipShell = 'terminal.integrated.commandsToSkipShell', AllowChords = 'terminal.integrated.allowChords', AllowMnemonics = 'terminal.integrated.allowMnemonics', + TabFocusMode = 'terminal.integrated.tabFocusMode', EnvMacOs = 'terminal.integrated.env.osx', EnvLinux = 'terminal.integrated.env.linux', EnvWindows = 'terminal.integrated.env.windows', @@ -112,8 +113,7 @@ export const enum TerminalSettingId { ShellIntegrationDecorationsEnabled = 'terminal.integrated.shellIntegration.decorationsEnabled', ShellIntegrationCommandHistory = 'terminal.integrated.shellIntegration.history', ShellIntegrationSuggestEnabled = 'terminal.integrated.shellIntegration.suggestEnabled', - SmoothScrolling = 'terminal.integrated.smoothScrolling', - AccessibleBufferContentEditable = 'terminal.integrated.accessibleBufferContentEditable' + SmoothScrolling = 'terminal.integrated.smoothScrolling' } export const enum TerminalLogConstants { diff --git a/src/vs/platform/terminal/node/ptyHostService.ts b/src/vs/platform/terminal/node/ptyHostService.ts index ee5790c59cc..3c019f0d160 100644 --- a/src/vs/platform/terminal/node/ptyHostService.ts +++ b/src/vs/platform/terminal/node/ptyHostService.ts @@ -11,7 +11,7 @@ import { ProxyChannel } from 'vs/base/parts/ipc/common/ipc'; import { Client, IIPCOptions } from 'vs/base/parts/ipc/node/ipc.cp'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IEnvironmentService, INativeEnvironmentService } from 'vs/platform/environment/common/environment'; -import { parsePtyHostPort } from 'vs/platform/environment/common/environmentService'; +import { parsePtyHostDebugPort } from 'vs/platform/environment/node/environmentService'; import { getResolvedShellEnv } from 'vs/platform/shell/node/shellEnv'; import { ILogService, ILoggerService } from 'vs/platform/log/common/log'; import { RequestStore } from 'vs/platform/terminal/common/requestStore'; @@ -149,7 +149,7 @@ export class PtyHostService extends Disposable implements IPtyService { } }; - const ptyHostDebug = parsePtyHostPort(this._environmentService.args, this._environmentService.isBuilt); + const ptyHostDebug = parsePtyHostDebugPort(this._environmentService.args, this._environmentService.isBuilt); if (ptyHostDebug) { if (ptyHostDebug.break && ptyHostDebug.port) { opts.debugBrk = ptyHostDebug.port; diff --git a/src/vs/platform/userDataProfile/electron-sandbox/userDataProfile.ts b/src/vs/platform/userDataProfile/common/userDataProfileIpc.ts similarity index 66% rename from src/vs/platform/userDataProfile/electron-sandbox/userDataProfile.ts rename to src/vs/platform/userDataProfile/common/userDataProfileIpc.ts index 60767bf59ef..ceaa5ec8fc8 100644 --- a/src/vs/platform/userDataProfile/electron-sandbox/userDataProfile.ts +++ b/src/vs/platform/userDataProfile/common/userDataProfileIpc.ts @@ -5,22 +5,59 @@ import { Emitter, Event } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; -import { joinPath } from 'vs/base/common/resources'; +import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc'; import { URI, UriDto } from 'vs/base/common/uri'; -import { IChannel } from 'vs/base/parts/ipc/common/ipc'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { IMainProcessService } from 'vs/platform/ipc/electron-sandbox/services'; import { DidChangeProfilesEvent, IUserDataProfile, IUserDataProfileOptions, IUserDataProfilesService, IUserDataProfileUpdateOptions, reviveProfile } from 'vs/platform/userDataProfile/common/userDataProfile'; import { IAnyWorkspaceIdentifier } from 'vs/platform/workspace/common/workspace'; +import { IURITransformer, transformIncomingURIs, transformOutgoingURIs } from 'vs/base/common/uriIpc'; -export class UserDataProfilesNativeService extends Disposable implements IUserDataProfilesService { +export class RemoteUserDataProfilesServiceChannel implements IServerChannel { + + constructor( + private readonly service: IUserDataProfilesService, + private readonly getUriTransformer: (requestContext: any) => IURITransformer + ) { } + + listen(context: any, event: string): Event { + const uriTransformer = this.getUriTransformer(context); + switch (event) { + case 'onDidChangeProfiles': return Event.map(this.service.onDidChangeProfiles, e => { + return { + all: e.all.map(p => transformOutgoingURIs({ ...p }, uriTransformer)), + added: e.added.map(p => transformOutgoingURIs({ ...p }, uriTransformer)), + removed: e.removed.map(p => transformOutgoingURIs({ ...p }, uriTransformer)), + updated: e.updated.map(p => transformOutgoingURIs({ ...p }, uriTransformer)) + }; + }); + } + throw new Error(`Invalid listen ${event}`); + } + + async call(context: any, command: string, args?: any): Promise { + const uriTransformer = this.getUriTransformer(context); + switch (command) { + case 'createProfile': { + const profile = await this.service.createProfile(args[0], args[1], args[2]); + return transformOutgoingURIs({ ...profile }, uriTransformer); + } + case 'updateProfile': { + let profile = reviveProfile(transformIncomingURIs(args[0], uriTransformer), this.service.profilesHome.scheme); + profile = await this.service.updateProfile(profile, args[1]); + return transformOutgoingURIs({ ...profile }, uriTransformer); + } + case 'removeProfile': { + const profile = reviveProfile(transformIncomingURIs(args[0], uriTransformer), this.service.profilesHome.scheme); + return this.service.removeProfile(profile); + } + } + throw new Error(`Invalid call ${command}`); + } +} + +export class UserDataProfilesService extends Disposable implements IUserDataProfilesService { readonly _serviceBrand: undefined; - private readonly channel: IChannel; - - readonly profilesHome: URI; - get defaultProfile(): IUserDataProfile { return this.profiles[0]; } private _profiles: IUserDataProfile[] = []; get profiles(): IUserDataProfile[] { return this._profiles; } @@ -34,12 +71,10 @@ export class UserDataProfilesNativeService extends Disposable implements IUserDa constructor( profiles: readonly UriDto[], - @IMainProcessService mainProcessService: IMainProcessService, - @IEnvironmentService environmentService: IEnvironmentService, + readonly profilesHome: URI, + private readonly channel: IChannel, ) { super(); - this.channel = mainProcessService.getChannel('userDataProfiles'); - this.profilesHome = joinPath(environmentService.userRoamingDataHome, 'profiles'); this._profiles = profiles.map(profile => reviveProfile(profile, this.profilesHome.scheme)); this._register(this.channel.listen('onDidChangeProfiles')(e => { const added = e.added.map(profile => reviveProfile(profile, this.profilesHome.scheme)); @@ -100,4 +135,3 @@ export class UserDataProfilesNativeService extends Disposable implements IUserDa } } - diff --git a/src/vs/platform/userDataSync/test/common/userDataAutoSyncService.test.ts b/src/vs/platform/userDataSync/test/common/userDataAutoSyncService.test.ts index db82505fc6d..7f6e72d2ff2 100644 --- a/src/vs/platform/userDataSync/test/common/userDataAutoSyncService.test.ts +++ b/src/vs/platform/userDataSync/test/common/userDataAutoSyncService.test.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; +import { timeout } from 'vs/base/common/async'; import { VSBuffer } from 'vs/base/common/buffer'; import { Event } from 'vs/base/common/event'; import { DisposableStore } from 'vs/base/common/lifecycle'; @@ -381,7 +382,7 @@ suite('UserDataAutoSyncService', () => { const errorPromise = Event.toPromise(testObject.onError); await testObject.sync(); - const e = await errorPromise; + const e = await Promise.race([errorPromise, timeout(0)]); assert.ok(e instanceof UserDataAutoSyncError); assert.deepStrictEqual((e).code, UserDataSyncErrorCode.SessionExpired); assert.deepStrictEqual(target.requests, [ diff --git a/src/vs/platform/utilityProcess/electron-main/utilityProcess.ts b/src/vs/platform/utilityProcess/electron-main/utilityProcess.ts index bd5d3f7d7b2..2321092b243 100644 --- a/src/vs/platform/utilityProcess/electron-main/utilityProcess.ts +++ b/src/vs/platform/utilityProcess/electron-main/utilityProcess.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { BrowserWindow, Details, MessageChannelMain, app } from 'electron'; +import { BrowserWindow, Details, app, MessageChannelMain, MessagePortMain } from 'electron'; import { Disposable } from 'vs/base/common/lifecycle'; import { Emitter, Event } from 'vs/base/common/event'; import { ILogService } from 'vs/platform/log/common/log'; @@ -15,22 +15,27 @@ import { IWindowsMainService } from 'vs/platform/windows/electron-main/windows'; import Severity from 'vs/base/common/severity'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { ILifecycleMainService } from 'vs/platform/lifecycle/electron-main/lifecycleMainService'; +import { removeDangerousEnvVariables } from 'vs/base/common/processes'; +import { deepClone } from 'vs/base/common/objects'; export interface IUtilityProcessConfiguration { - // --- message port response related - - readonly responseWindowId: number; - readonly responseChannel: string; - readonly responseNonce: string; - - // --- utility process options - /** * A way to group utility processes of same type together. */ readonly type: string; + /** + * The entry point to load in the utility process. + */ + readonly entryPoint: string; + + /** + * An optional serializable object to be sent into the utility process + * as first message alongside the message port. + */ + readonly payload?: unknown; + /** * Environment key-value pairs. Default is `process.env`. */ @@ -58,6 +63,24 @@ export interface IUtilityProcessConfiguration { */ readonly correlationId?: string; + /** + * Optional pid of the parent process. If set, the + * utility process will be terminated when the parent + * process exits. + */ + readonly parentLifecycleBound?: number; +} + +export interface IWindowUtilityProcessConfiguration extends IUtilityProcessConfiguration { + + // --- message port response related + + readonly responseWindowId: number; + readonly responseChannel: string; + readonly responseNonce: string; + + // --- utility process options + /** * If set to `true`, will terminate the utility process * when the associated browser window closes or reloads. @@ -122,14 +145,13 @@ export class UtilityProcess extends Disposable { constructor( @ILogService private readonly logService: ILogService, - @IWindowsMainService private readonly windowsMainService: IWindowsMainService, @ITelemetryService private readonly telemetryService: ITelemetryService, - @ILifecycleMainService private readonly lifecycleMainService: ILifecycleMainService + @ILifecycleMainService protected readonly lifecycleMainService: ILifecycleMainService ) { super(); } - private log(msg: string, severity: Severity): void { + protected log(msg: string, severity: Severity): void { let logMsg: string; if (this.configuration?.correlationId) { logMsg = `[UtilityProcess id: ${this.configuration?.correlationId}, type: ${this.configuration?.type}, pid: ${this.processPid ?? ''}]: ${msg}`; @@ -150,28 +172,32 @@ export class UtilityProcess extends Disposable { } } - private validateCanStart(configuration: IUtilityProcessConfiguration): BrowserWindow | undefined { + private validateCanStart(): boolean { if (!canUseUtilityProcess) { throw new Error('Cannot use UtilityProcess API from Electron!'); } if (this.process) { this.log('Cannot start utility process because it is already running...', Severity.Error); - return undefined; + + return false; } - const responseWindow = this.windowsMainService.getWindowById(configuration.responseWindowId)?.win; - if (!responseWindow || responseWindow.isDestroyed() || responseWindow.webContents.isDestroyed()) { - this.log('Refusing to start utility process because requesting window cannot be found or is destroyed...', Severity.Error); - return undefined; - } - - return responseWindow; + return true; } start(configuration: IUtilityProcessConfiguration): boolean { - const responseWindow = this.validateCanStart(configuration); - if (!responseWindow) { + const started = this.doStart(configuration, false); + + if (started && configuration.payload) { + this.postMessage(configuration.payload); + } + + return started; + } + + protected doStart(configuration: IUtilityProcessConfiguration, isWindowSandboxed: boolean): boolean { + if (!this.validateCanStart()) { return false; } @@ -183,15 +209,7 @@ export class UtilityProcess extends Disposable { const execArgv = [...this.configuration.execArgv ?? [], `--vscode-utility-kind=${this.configuration.type}`]; const allowLoadingUnsignedLibraries = this.configuration.allowLoadingUnsignedLibraries; const stdio = 'pipe'; - - let env: { [key: string]: any } | undefined = this.configuration.env; - if (env) { - env = { ...env }; // make a copy since we may be going to mutate it - - for (const key of Object.keys(env)) { - env[key] = String(env[key]); // make sure all values are strings, otherwise the process will not start - } - } + const env = this.createEnv(configuration); this.log('creating new...', Severity.Info); @@ -205,23 +223,33 @@ export class UtilityProcess extends Disposable { }); // Register to events - this.registerListeners(responseWindow, this.process, this.configuration, serviceName); - - // Exchange message ports - this.exchangeMessagePorts(this.process, this.configuration, responseWindow); + this.registerListeners(this.process, this.configuration, serviceName, isWindowSandboxed); return true; } - private registerListeners(window: BrowserWindow, process: UtilityProcessProposedApi.UtilityProcess, configuration: IUtilityProcessConfiguration, serviceName: string): void { + private createEnv(configuration: IUtilityProcessConfiguration): { [key: string]: any } { + const env: { [key: string]: any } = configuration.env ? { ...configuration.env } : { ...deepClone(process.env) }; - // If the lifecycle of the utility process is bound to the window, - // we kill the process if the window closes or changes - if (configuration.windowLifecycleBound) { - this._register(Event.filter(this.lifecycleMainService.onWillLoadWindow, e => e.window.win === window)(() => this.kill())); - this._register(Event.fromNodeEventEmitter(window, 'closed')(() => this.kill())); + // Apply support environment variables from config + env['VSCODE_AMD_ENTRYPOINT'] = configuration.entryPoint; + if (typeof configuration.parentLifecycleBound === 'number') { + env['VSCODE_PARENT_PID'] = String(configuration.parentLifecycleBound); } + // Remove any environment variables that are not allowed + removeDangerousEnvVariables(env); + + // Ensure all values are strings, otherwise the process will not start + for (const key of Object.keys(env)) { + env[key] = String(env[key]); + } + + return env; + } + + private registerListeners(process: UtilityProcessProposedApi.UtilityProcess, configuration: IUtilityProcessConfiguration, serviceName: string, isWindowSandboxed: boolean): void { + // Stdout if (process.stdout) { const stdoutDecoder = new StringDecoder('utf-8'); @@ -234,7 +262,7 @@ export class UtilityProcess extends Disposable { this._register(Event.fromNodeEventEmitter(process.stderr, 'data')(chunk => this._onStderr.fire(typeof chunk === 'string' ? chunk : stderrDecoder.write(chunk)))); } - //Messages + // Messages this._register(Event.fromNodeEventEmitter(process, 'message')(msg => this._onMessage.fire(msg))); // Spawn @@ -264,6 +292,7 @@ export class UtilityProcess extends Disposable { type UtilityProcessCrashClassification = { type: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The type of utility process to understand the origin of the crash better.' }; reason: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The reason of the utility process crash to understand the nature of the crash better.' }; + sandboxed: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'If the window for the utility process was sandboxed or not.' }; code: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'The exit code of the utility process to understand the nature of the crash better' }; owner: 'bpasero'; comment: 'Provides insight into reasons the utility process crashed.'; @@ -272,8 +301,14 @@ export class UtilityProcess extends Disposable { type: string; reason: string; code: number; + sandboxed: string; }; - this.telemetryService.publicLog2('utilityprocesscrash', { type: configuration.type, reason: details.reason, code: details.exitCode }); + this.telemetryService.publicLog2('utilityprocesscrash', { + type: configuration.type, + reason: details.reason, + code: details.exitCode, + sandboxed: isWindowSandboxed ? '1' : '0' // TODO@bpasero remove this once sandbox is enabled by default + }); // Event this._onCrash.fire({ pid: this.processPid!, code: details.exitCode, reason: details.reason }); @@ -284,11 +319,29 @@ export class UtilityProcess extends Disposable { })); } - private exchangeMessagePorts(process: UtilityProcessProposedApi.UtilityProcess, configuration: IUtilityProcessConfiguration, responseWindow: BrowserWindow) { - const { port1: windowPort, port2: utilityProcessPort } = new MessageChannelMain(); + once(message: unknown, callback: () => void): void { + const disposable = this._register(this._onMessage.event(msg => { + if (msg === message) { + disposable.dispose(); - process.postMessage('null', [utilityProcessPort]); - responseWindow.webContents.postMessage(configuration.responseChannel, configuration.responseNonce, [windowPort]); + callback(); + } + })); + } + + postMessage(message: unknown, transfer?: Electron.MessagePortMain[]): void { + if (!this.process) { + return; // already killed, crashed or never started + } + + this.process.postMessage(message, transfer); + } + + connect(payload?: unknown): MessagePortMain { + const { port1: outPort, port2: utilityProcessPort } = new MessageChannelMain(); + this.postMessage(payload, [utilityProcessPort]); + + return outPort; } enableInspectPort(): boolean { @@ -335,16 +388,62 @@ export class UtilityProcess extends Disposable { async waitForExit(maxWaitTimeMs: number): Promise { if (!this.process) { - this.log('no running process to wait for exit', Severity.Warning); - return; + return; // already killed, crashed or never started } this.log('waiting to exit...', Severity.Info); await Promise.race([Event.toPromise(this.onExit), timeout(maxWaitTimeMs)]); if (this.process) { - this.log('did not exit within ${maxWaitTimeMs}ms, will kill it now...', Severity.Info); + this.log(`did not exit within ${maxWaitTimeMs}ms, will kill it now...`, Severity.Info); this.kill(); } } } + +export class WindowUtilityProcess extends UtilityProcess { + + constructor( + @ILogService logService: ILogService, + @IWindowsMainService private readonly windowsMainService: IWindowsMainService, + @ITelemetryService telemetryService: ITelemetryService, + @ILifecycleMainService lifecycleMainService: ILifecycleMainService + ) { + super(logService, telemetryService, lifecycleMainService); + } + + override start(configuration: IWindowUtilityProcessConfiguration): boolean { + const responseWindow = this.windowsMainService.getWindowById(configuration.responseWindowId); + if (!responseWindow?.win || responseWindow.win.isDestroyed() || responseWindow.win.webContents.isDestroyed()) { + this.log('Refusing to start utility process because requesting window cannot be found or is destroyed...', Severity.Error); + + return true; + } + + // Start utility process + const started = super.doStart(configuration, responseWindow.isSandboxed); + if (!started) { + return false; + } + + // Register to window events + this.registerWindowListeners(responseWindow.win, configuration); + + // Establish & exchange message ports + const windowPort = this.connect(configuration.payload); + responseWindow.win.webContents.postMessage(configuration.responseChannel, configuration.responseNonce, [windowPort]); + + return true; + } + + private registerWindowListeners(window: BrowserWindow, configuration: IWindowUtilityProcessConfiguration): void { + + // If the lifecycle of the utility process is bound to the window, + // we kill the process if the window closes or changes + + if (configuration.windowLifecycleBound) { + this._register(Event.filter(this.lifecycleMainService.onWillLoadWindow, e => e.window.win === window)(() => this.kill())); + this._register(Event.fromNodeEventEmitter(window, 'closed')(() => this.kill())); + } + } +} diff --git a/src/vs/platform/utilityProcess/electron-main/utilityProcessWorkerMainService.ts b/src/vs/platform/utilityProcess/electron-main/utilityProcessWorkerMainService.ts index f43524df304..b5e54ffa725 100644 --- a/src/vs/platform/utilityProcess/electron-main/utilityProcessWorkerMainService.ts +++ b/src/vs/platform/utilityProcess/electron-main/utilityProcessWorkerMainService.ts @@ -8,10 +8,8 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation' import { ILogService } from 'vs/platform/log/common/log'; import { IUtilityProcessWorkerCreateConfiguration, IOnDidTerminateUtilityrocessWorkerProcess, IUtilityProcessWorkerConfiguration, IUtilityProcessWorkerProcessExit, IUtilityProcessWorkerService } from 'vs/platform/utilityProcess/common/utilityProcessWorkerService'; import { IWindowsMainService } from 'vs/platform/windows/electron-main/windows'; -import { UtilityProcess } from 'vs/platform/utilityProcess/electron-main/utilityProcess'; +import { WindowUtilityProcess } from 'vs/platform/utilityProcess/electron-main/utilityProcess'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { deepClone } from 'vs/base/common/objects'; -import { removeDangerousEnvVariables } from 'vs/base/common/processes'; import { hash } from 'vs/base/common/hash'; import { Event, Emitter } from 'vs/base/common/event'; import { DeferredPromise } from 'vs/base/common/async'; @@ -94,7 +92,7 @@ class UtilityProcessWorker extends Disposable { private readonly _onDidTerminate = this._register(new Emitter()); readonly onDidTerminate = this._onDidTerminate.event; - private readonly utilityProcess = new UtilityProcess(this.logService, this.windowsMainService, this.telemetryService, this.lifecycleMainService); + private readonly utilityProcess = new WindowUtilityProcess(this.logService, this.windowsMainService, this.telemetryService, this.lifecycleMainService); constructor( @ILogService private readonly logService: ILogService, @@ -114,32 +112,21 @@ class UtilityProcessWorker extends Disposable { } spawn(): boolean { + const window = this.windowsMainService.getWindowById(this.configuration.reply.windowId); + const windowPid = window?.win?.webContents.getOSProcessId(); + return this.utilityProcess.start({ + type: this.configuration.process.type, + entryPoint: this.configuration.process.moduleId, + parentLifecycleBound: windowPid, windowLifecycleBound: true, correlationId: `${this.configuration.reply.windowId}`, responseWindowId: this.configuration.reply.windowId, responseChannel: this.configuration.reply.channel, - responseNonce: this.configuration.reply.nonce, - type: this.configuration.process.type, - env: this.getEnv() + responseNonce: this.configuration.reply.nonce }); } - private getEnv(): NodeJS.ProcessEnv { - const env: NodeJS.ProcessEnv = { - ...deepClone(process.env), - VSCODE_AMD_ENTRYPOINT: this.configuration.process.moduleId, - VSCODE_PIPE_LOGGING: 'true', - VSCODE_VERBOSE_LOGGING: 'true', - VSCODE_PARENT_PID: String(process.pid) - }; - - // Sanitize environment - removeDangerousEnvVariables(env); - - return env; - } - kill() { this.utilityProcess.kill(); } diff --git a/src/vs/platform/window/common/window.ts b/src/vs/platform/window/common/window.ts index ccb87376d87..9a6403ae949 100644 --- a/src/vs/platform/window/common/window.ts +++ b/src/vs/platform/window/common/window.ts @@ -137,7 +137,7 @@ export interface IWindowSettings { readonly enableMenuBarMnemonics: boolean; readonly closeWhenEmpty: boolean; readonly clickThroughInactive: boolean; - readonly experimental?: { useSandbox: boolean }; + readonly experimental?: { useSandbox: boolean; sharedProcessUseUtilityProcess: boolean }; } export function getTitleBarStyle(configurationService: IConfigurationService): 'native' | 'custom' { @@ -285,6 +285,7 @@ export interface INativeWindowConfiguration extends IWindowConfiguration, Native backupPath?: string; profiles: { + home: UriComponents; all: readonly UriDto[]; profile: UriDto; }; diff --git a/src/vs/platform/window/electron-main/window.ts b/src/vs/platform/window/electron-main/window.ts index 8330d6f39a5..0ae587f5894 100644 --- a/src/vs/platform/window/electron-main/window.ts +++ b/src/vs/platform/window/electron-main/window.ts @@ -44,6 +44,8 @@ export interface ICodeWindow extends IDisposable { ready(): Promise; setReady(): void; + readonly isSandboxed: boolean; + addTabbedWindow(window: ICodeWindow): void; load(config: INativeWindowConfiguration, options?: { isReload?: boolean }): void; diff --git a/src/vs/platform/windows/electron-main/windowImpl.ts b/src/vs/platform/windows/electron-main/windowImpl.ts index e2f045991fc..4af29bb8635 100644 --- a/src/vs/platform/windows/electron-main/windowImpl.ts +++ b/src/vs/platform/windows/electron-main/windowImpl.ts @@ -125,6 +125,9 @@ export class CodeWindow extends Disposable implements ICodeWindow { private _lastFocusTime = -1; get lastFocusTime(): number { return this._lastFocusTime; } + private _isSandboxed = false; + get isSandboxed(): boolean { return this._isSandboxed; } + get backupPath(): string | undefined { return this._config?.backupPath; } get openedWorkspace(): IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | undefined { return this._config?.workspace; } @@ -233,6 +236,8 @@ export class CodeWindow extends Disposable implements ICodeWindow { useSandbox = typeof this.productService.quality === 'string' && this.productService.quality !== 'stable'; } + this._isSandboxed = useSandbox; + const options: BrowserWindowConstructorOptions & { experimentalDarkMode: boolean } = { width: this.windowState.width, height: this.windowState.height, @@ -1082,7 +1087,8 @@ export class CodeWindow extends Disposable implements ICodeWindow { configuration.continueOn = this.environmentMainService.continueOn; configuration.profiles = { all: this.userDataProfilesService.profiles, - profile: this.profile || this.userDataProfilesService.defaultProfile + profile: this.profile || this.userDataProfilesService.defaultProfile, + home: this.userDataProfilesService.profilesHome }; configuration.logLevel = this.loggerMainService.getLogLevel(); configuration.loggers = { diff --git a/src/vs/platform/windows/electron-main/windowsMainService.ts b/src/vs/platform/windows/electron-main/windowsMainService.ts index b2282599855..62fa52e0cbb 100644 --- a/src/vs/platform/windows/electron-main/windowsMainService.ts +++ b/src/vs/platform/windows/electron-main/windowsMainService.ts @@ -54,6 +54,7 @@ import { IUserDataProfile } from 'vs/platform/userDataProfile/common/userDataPro import { IPolicyService } from 'vs/platform/policy/common/policy'; import { IUserDataProfilesMainService } from 'vs/platform/userDataProfile/electron-main/userDataProfile'; import { ILoggerMainService } from 'vs/platform/log/electron-main/loggerService'; +import { canUseUtilityProcess } from 'vs/base/parts/sandbox/electron-main/electronTypes'; //#region Helper Interfaces @@ -1348,6 +1349,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic backupPath: options.emptyWindowBackupInfo ? join(this.environmentMainService.backupHome, options.emptyWindowBackupInfo.backupFolder) : undefined, profiles: { + home: this.userDataProfilesMainService.profilesHome, all: this.userDataProfilesMainService.profiles, // Set to default profile first and resolve and update the profile // only after the workspace-backup is registered. @@ -1388,7 +1390,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic policiesData: this.policyService.serialize(), continueOn: this.environmentMainService.continueOn, - preferUtilityProcess: filesConfig?.experimental?.watcherUseUtilityProcess ?? false + preferUtilityProcess: canUseUtilityProcess ? (filesConfig?.experimental?.watcherUseUtilityProcess ?? windowConfig?.experimental?.sharedProcessUseUtilityProcess ?? false) : false }; // New window diff --git a/src/vs/platform/windows/test/electron-main/windowsFinder.test.ts b/src/vs/platform/windows/test/electron-main/windowsFinder.test.ts index faebfc38595..03f175eda8e 100644 --- a/src/vs/platform/windows/test/electron-main/windowsFinder.test.ts +++ b/src/vs/platform/windows/test/electron-main/windowsFinder.test.ts @@ -49,6 +49,7 @@ suite('WindowsFinder', () => { lastFocusTime = options.lastFocusTime; isFullScreen = false; isReady = true; + isSandboxed = false; ready(): Promise { throw new Error('Method not implemented.'); } setReady(): void { throw new Error('Method not implemented.'); } diff --git a/src/vs/server/node/remoteAgentEnvironmentImpl.ts b/src/vs/server/node/remoteAgentEnvironmentImpl.ts index 19d01ff49c8..a991de0deae 100644 --- a/src/vs/server/node/remoteAgentEnvironmentImpl.ts +++ b/src/vs/server/node/remoteAgentEnvironmentImpl.ts @@ -8,63 +8,29 @@ import * as platform from 'vs/base/common/platform'; import * as performance from 'vs/base/common/performance'; import { URI } from 'vs/base/common/uri'; import { createURITransformer } from 'vs/workbench/api/node/uriTransformer'; -import { IRemoteAgentEnvironmentDTO, IGetEnvironmentDataArguments, IScanExtensionsArguments, IScanSingleExtensionArguments, IGetExtensionHostExitInfoArguments } from 'vs/workbench/services/remote/common/remoteAgentEnvironmentChannel'; -import { Schemas } from 'vs/base/common/network'; +import { IRemoteAgentEnvironmentDTO, IGetEnvironmentDataArguments, IGetExtensionHostExitInfoArguments } from 'vs/workbench/services/remote/common/remoteAgentEnvironmentChannel'; import { IServerEnvironmentService } from 'vs/server/node/serverEnvironmentService'; import { IServerChannel } from 'vs/base/parts/ipc/common/ipc'; -import { ExtensionType, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { transformOutgoingURIs } from 'vs/base/common/uriIpc'; -import { ILogService } from 'vs/platform/log/common/log'; -import { ContextKeyExpr, ContextKeyDefinedExpr, ContextKeyNotExpr, ContextKeyEqualsExpr, ContextKeyNotEqualsExpr, ContextKeyRegexExpr, IContextKeyExprMapper, ContextKeyExpression, ContextKeyInExpr, ContextKeyGreaterExpr, ContextKeyGreaterEqualsExpr, ContextKeySmallerExpr, ContextKeySmallerEqualsExpr, ContextKeyNotInExpr } from 'vs/platform/contextkey/common/contextkey'; import { listProcesses } from 'vs/base/node/ps'; import { getMachineInfo, collectWorkspaceStats } from 'vs/platform/diagnostics/node/diagnosticsService'; import { IDiagnosticInfoOptions, IDiagnosticInfo } from 'vs/platform/diagnostics/common/diagnostics'; -import { basename, isAbsolute, join, resolve } from 'vs/base/common/path'; +import { basename, join } from 'vs/base/common/path'; import { ProcessItem } from 'vs/base/common/processes'; -import { InstallOptions } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { cwd } from 'vs/base/common/process'; import { ServerConnectionToken, ServerConnectionTokenType } from 'vs/server/node/serverConnectionToken'; import { IExtensionHostStatusService } from 'vs/server/node/extensionHostStatusService'; -import { IExtensionsScannerService, toExtensionDescription } from 'vs/platform/extensionManagement/common/extensionsScannerService'; -import { dedupExtensions } from 'vs/workbench/services/extensions/common/extensionsUtil'; import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; -import { ExtensionManagementCLI } from 'vs/platform/extensionManagement/common/extensionManagementCLI'; export class RemoteAgentEnvironmentChannel implements IServerChannel { private static _namePool = 1; - private readonly whenExtensionsReady: Promise; - constructor( private readonly _connectionToken: ServerConnectionToken, private readonly _environmentService: IServerEnvironmentService, private readonly _userDataProfilesService: IUserDataProfilesService, - extensionManagementCLI: ExtensionManagementCLI, - private readonly _logService: ILogService, private readonly _extensionHostStatusService: IExtensionHostStatusService, - private readonly _extensionsScannerService: IExtensionsScannerService, ) { - if (_environmentService.args['install-builtin-extension']) { - const installOptions: InstallOptions = { isMachineScoped: !!_environmentService.args['do-not-sync'], installPreReleaseVersion: !!_environmentService.args['pre-release'] }; - performance.mark('code/server/willInstallBuiltinExtensions'); - this.whenExtensionsReady = extensionManagementCLI.installExtensions([], _environmentService.args['install-builtin-extension'], installOptions, !!_environmentService.args['force']) - .then(() => performance.mark('code/server/didInstallBuiltinExtensions'), error => { - _logService.error(error); - }); - } else { - this.whenExtensionsReady = Promise.resolve(); - } - - const extensionsToInstall = _environmentService.args['install-extension']; - if (extensionsToInstall) { - const idsOrVSIX = extensionsToInstall.map(input => /\.vsix$/i.test(input) ? URI.file(isAbsolute(input) ? input : join(cwd(), input)) : input); - this.whenExtensionsReady - .then(() => extensionManagementCLI.installExtensions(idsOrVSIX, [], { isMachineScoped: !!_environmentService.args['do-not-sync'], installPreReleaseVersion: !!_environmentService.args['pre-release'] }, !!_environmentService.args['force'])) - .then(null, error => { - _logService.error(error); - }); - } } async call(_: any, command: string, arg?: any): Promise { @@ -74,7 +40,7 @@ export class RemoteAgentEnvironmentChannel implements IServerChannel { const args = arg; const uriTransformer = createURITransformer(args.remoteAuthority); - let environmentData = await this._getEnvironmentData(); + let environmentData = await this._getEnvironmentData(args.profile); environmentData = transformOutgoingURIs(environmentData, uriTransformer); return environmentData; @@ -85,59 +51,6 @@ export class RemoteAgentEnvironmentChannel implements IServerChannel { return this._extensionHostStatusService.getExitInfo(args.reconnectionToken); } - case 'whenExtensionsReady': { - await this.whenExtensionsReady; - return; - } - - case 'scanExtensions': { - await this.whenExtensionsReady; - performance.mark('code/server/willScanExtensions'); - - const args = arg; - const language = args.language; - this._logService.trace(`Scanning extensions using UI language: ${language}`); - const uriTransformer = createURITransformer(args.remoteAuthority); - - const extensionDevelopmentLocations = args.extensionDevelopmentPath && args.extensionDevelopmentPath.map(url => URI.revive(uriTransformer.transformIncoming(url))); - const extensionDevelopmentPath = extensionDevelopmentLocations ? extensionDevelopmentLocations.filter(url => url.scheme === Schemas.file).map(url => url.fsPath) : undefined; - - let extensions = await this._scanExtensions(language, extensionDevelopmentPath); - extensions = transformOutgoingURIs(extensions, uriTransformer); - - this._logService.trace('Scanned Extensions', extensions); - RemoteAgentEnvironmentChannel._massageWhenConditions(extensions); - - performance.mark('code/server/didScanExtensions'); - return extensions; - } - - case 'scanSingleExtension': { - await this.whenExtensionsReady; - const args = arg; - const language = args.language; - const isBuiltin = args.isBuiltin; - const uriTransformer = createURITransformer(args.remoteAuthority); - const extensionLocation = URI.revive(uriTransformer.transformIncoming(args.extensionLocation)); - const extensionPath = extensionLocation.scheme === Schemas.file ? extensionLocation.fsPath : null; - - if (!extensionPath) { - return null; - } - - let extension = await this._scanSingleExtension(extensionPath, isBuiltin, language); - - if (!extension) { - return null; - } - - extension = transformOutgoingURIs(extension, uriTransformer); - - RemoteAgentEnvironmentChannel._massageWhenConditions([extension]); - - return extension; - } - case 'getDiagnosticInfo': { const options = arg; const diagnosticInfo: IDiagnosticInfo = { @@ -178,120 +91,10 @@ export class RemoteAgentEnvironmentChannel implements IServerChannel { throw new Error('Not supported'); } - private static _massageWhenConditions(extensions: IExtensionDescription[]): void { - // Massage "when" conditions which mention `resourceScheme` - - interface WhenUser { when?: string } - - interface LocWhenUser { [loc: string]: WhenUser[] } - - const _mapResourceSchemeValue = (value: string, isRegex: boolean): string => { - // console.log(`_mapResourceSchemeValue: ${value}, ${isRegex}`); - return value.replace(/file/g, 'vscode-remote'); - }; - - const _mapResourceRegExpValue = (value: RegExp): RegExp => { - let flags = ''; - flags += value.global ? 'g' : ''; - flags += value.ignoreCase ? 'i' : ''; - flags += value.multiline ? 'm' : ''; - return new RegExp(_mapResourceSchemeValue(value.source, true), flags); - }; - - const _exprKeyMapper = new class implements IContextKeyExprMapper { - mapDefined(key: string): ContextKeyExpression { - return ContextKeyDefinedExpr.create(key); - } - mapNot(key: string): ContextKeyExpression { - return ContextKeyNotExpr.create(key); - } - mapEquals(key: string, value: any): ContextKeyExpression { - if (key === 'resourceScheme' && typeof value === 'string') { - return ContextKeyEqualsExpr.create(key, _mapResourceSchemeValue(value, false)); - } else { - return ContextKeyEqualsExpr.create(key, value); - } - } - mapNotEquals(key: string, value: any): ContextKeyExpression { - if (key === 'resourceScheme' && typeof value === 'string') { - return ContextKeyNotEqualsExpr.create(key, _mapResourceSchemeValue(value, false)); - } else { - return ContextKeyNotEqualsExpr.create(key, value); - } - } - mapGreater(key: string, value: any): ContextKeyExpression { - return ContextKeyGreaterExpr.create(key, value); - } - mapGreaterEquals(key: string, value: any): ContextKeyExpression { - return ContextKeyGreaterEqualsExpr.create(key, value); - } - mapSmaller(key: string, value: any): ContextKeyExpression { - return ContextKeySmallerExpr.create(key, value); - } - mapSmallerEquals(key: string, value: any): ContextKeyExpression { - return ContextKeySmallerEqualsExpr.create(key, value); - } - mapRegex(key: string, regexp: RegExp | null): ContextKeyRegexExpr { - if (key === 'resourceScheme' && regexp) { - return ContextKeyRegexExpr.create(key, _mapResourceRegExpValue(regexp)); - } else { - return ContextKeyRegexExpr.create(key, regexp); - } - } - mapIn(key: string, valueKey: string): ContextKeyInExpr { - return ContextKeyInExpr.create(key, valueKey); - } - mapNotIn(key: string, valueKey: string): ContextKeyNotInExpr { - return ContextKeyNotInExpr.create(key, valueKey); - } - }; - - const _massageWhenUser = (element: WhenUser) => { - if (!element || !element.when || !/resourceScheme/.test(element.when)) { - return; - } - - const expr = ContextKeyExpr.deserialize(element.when); - if (!expr) { - return; - } - - const massaged = expr.map(_exprKeyMapper); - element.when = massaged.serialize(); - }; - - const _massageWhenUserArr = (elements: WhenUser[] | WhenUser) => { - if (Array.isArray(elements)) { - for (const element of elements) { - _massageWhenUser(element); - } - } else { - _massageWhenUser(elements); - } - }; - - const _massageLocWhenUser = (target: LocWhenUser) => { - for (const loc in target) { - _massageWhenUserArr(target[loc]); - } - }; - - extensions.forEach((extension) => { - if (extension.contributes) { - if (extension.contributes.menus) { - _massageLocWhenUser(extension.contributes.menus); - } - if (extension.contributes.keybindings) { - _massageWhenUserArr(extension.contributes.keybindings); - } - if (extension.contributes.views) { - _massageLocWhenUser(extension.contributes.views); - } - } - }); - } - - private async _getEnvironmentData(): Promise { + private async _getEnvironmentData(profile?: string): Promise { + if (profile && !this._userDataProfilesService.profiles.some(p => p.id === profile)) { + await this._userDataProfilesService.createProfile(profile, profile); + } return { pid: process.pid, connectionToken: (this._connectionToken.type !== ServerConnectionTokenType.None ? this._connectionToken.value : ''), @@ -306,46 +109,12 @@ export class RemoteAgentEnvironmentChannel implements IServerChannel { os: platform.OS, arch: process.arch, marks: performance.getMarks(), - useHostProxy: !!this._environmentService.args['use-host-proxy'] + useHostProxy: !!this._environmentService.args['use-host-proxy'], + profiles: { + home: this._userDataProfilesService.profilesHome, + all: [...this._userDataProfilesService.profiles].map(profile => ({ ...profile })) + } }; } - private async _scanExtensions(language: string, extensionDevelopmentPath?: string[]): Promise { - // Ensure that the language packs are available - - const [builtinExtensions, installedExtensions, developedExtensions] = await Promise.all([ - this._scanBuiltinExtensions(language), - this._scanInstalledExtensions(language), - this._scanDevelopedExtensions(language, extensionDevelopmentPath) - ]); - - return dedupExtensions(builtinExtensions, installedExtensions, developedExtensions, this._logService); - } - - private async _scanDevelopedExtensions(language: string, extensionDevelopmentPaths?: string[]): Promise { - if (extensionDevelopmentPaths) { - return (await Promise.all(extensionDevelopmentPaths.map(extensionDevelopmentPath => this._extensionsScannerService.scanOneOrMultipleExtensions(URI.file(resolve(extensionDevelopmentPath)), ExtensionType.User, { language })))) - .flat() - .map(e => toExtensionDescription(e, true)); - } - return []; - } - - private async _scanBuiltinExtensions(language: string): Promise { - const scannedExtensions = await this._extensionsScannerService.scanSystemExtensions({ language, useCache: true }); - return scannedExtensions.map(e => toExtensionDescription(e, false)); - } - - private async _scanInstalledExtensions(language: string): Promise { - const scannedExtensions = await this._extensionsScannerService.scanUserExtensions({ profileLocation: this._userDataProfilesService.defaultProfile.extensionsResource, language, useCache: true }); - return scannedExtensions.map(e => toExtensionDescription(e, false)); - } - - private async _scanSingleExtension(extensionPath: string, isBuiltin: boolean, language: string): Promise { - const extensionLocation = URI.file(resolve(extensionPath)); - const type = isBuiltin ? ExtensionType.System : ExtensionType.User; - const scannedExtension = await this._extensionsScannerService.scanExistingExtension(extensionLocation, type, { language }); - return scannedExtension ? toExtensionDescription(scannedExtension, false) : null; - } - } diff --git a/src/vs/server/node/remoteExtensionsScanner.ts b/src/vs/server/node/remoteExtensionsScanner.ts new file mode 100644 index 00000000000..d11da3a2d6d --- /dev/null +++ b/src/vs/server/node/remoteExtensionsScanner.ts @@ -0,0 +1,324 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { isAbsolute, join, resolve } from 'vs/base/common/path'; +import * as platform from 'vs/base/common/platform'; +import { cwd } from 'vs/base/common/process'; +import { URI } from 'vs/base/common/uri'; +import * as performance from 'vs/base/common/performance'; +import { Event } from 'vs/base/common/event'; +import { IURITransformer, transformOutgoingURIs } from 'vs/base/common/uriIpc'; +import { IServerChannel } from 'vs/base/parts/ipc/common/ipc'; +import { ContextKeyDefinedExpr, ContextKeyEqualsExpr, ContextKeyExpr, ContextKeyExpression, ContextKeyGreaterEqualsExpr, ContextKeyGreaterExpr, ContextKeyInExpr, ContextKeyNotEqualsExpr, ContextKeyNotExpr, ContextKeyNotInExpr, ContextKeyRegexExpr, ContextKeySmallerEqualsExpr, ContextKeySmallerExpr, IContextKeyExprMapper } from 'vs/platform/contextkey/common/contextkey'; +import { IExtensionGalleryService, InstallOptions } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { ExtensionManagementCLI } from 'vs/platform/extensionManagement/common/extensionManagementCLI'; +import { IExtensionsScannerService, toExtensionDescription } from 'vs/platform/extensionManagement/common/extensionsScannerService'; +import { ExtensionType, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; +import { IServerEnvironmentService } from 'vs/server/node/serverEnvironmentService'; +import { dedupExtensions } from 'vs/workbench/services/extensions/common/extensionsUtil'; +import { Schemas } from 'vs/base/common/network'; +import { IRemoteExtensionsScannerService } from 'vs/platform/remote/common/remoteExtensionsScanner'; +import { ILanguagePackService } from 'vs/platform/languagePacks/common/languagePacks'; + +export class RemoteExtensionsScannerService implements IRemoteExtensionsScannerService { + + readonly _serviceBrand: undefined; + + private readonly _whenExtensionsReady: Promise; + + constructor( + private readonly _extensionManagementCLI: ExtensionManagementCLI, + environmentService: IServerEnvironmentService, + private readonly _userDataProfilesService: IUserDataProfilesService, + private readonly _extensionsScannerService: IExtensionsScannerService, + private readonly _logService: ILogService, + private readonly _extensionGalleryService: IExtensionGalleryService, + private readonly _languagePackService: ILanguagePackService + ) { + if (environmentService.args['install-builtin-extension']) { + const installOptions: InstallOptions = { isMachineScoped: !!environmentService.args['do-not-sync'], installPreReleaseVersion: !!environmentService.args['pre-release'] }; + performance.mark('code/server/willInstallBuiltinExtensions'); + this._whenExtensionsReady = _extensionManagementCLI.installExtensions([], environmentService.args['install-builtin-extension'], installOptions, !!environmentService.args['force']) + .then(() => performance.mark('code/server/didInstallBuiltinExtensions'), error => { + _logService.error(error); + }); + } else { + this._whenExtensionsReady = Promise.resolve(); + } + + const extensionsToInstall = environmentService.args['install-extension']; + if (extensionsToInstall) { + const idsOrVSIX = extensionsToInstall.map(input => /\.vsix$/i.test(input) ? URI.file(isAbsolute(input) ? input : join(cwd(), input)) : input); + this._whenExtensionsReady + .then(() => _extensionManagementCLI.installExtensions(idsOrVSIX, [], { isMachineScoped: !!environmentService.args['do-not-sync'], installPreReleaseVersion: !!environmentService.args['pre-release'] }, !!environmentService.args['force'])) + .then(null, error => { + _logService.error(error); + }); + } + } + + whenExtensionsReady(): Promise { + return this._whenExtensionsReady; + } + + async scanExtensions(language?: string, profileLocation?: URI, extensionDevelopmentLocations?: URI[], languagePackId?: string): Promise { + await this.whenExtensionsReady(); + + performance.mark('code/server/willScanExtensions'); + + this._logService.trace(`Scanning extensions using UI language: ${language}`); + + const extensionDevelopmentPaths = extensionDevelopmentLocations ? extensionDevelopmentLocations.filter(url => url.scheme === Schemas.file).map(url => url.fsPath) : undefined; + profileLocation = profileLocation ?? this._userDataProfilesService.defaultProfile.extensionsResource; + + const extensions = await this._scanExtensions(profileLocation, language ?? platform.language, extensionDevelopmentPaths, languagePackId); + + this._logService.trace('Scanned Extensions', extensions); + this._massageWhenConditions(extensions); + + performance.mark('code/server/didScanExtensions'); + return extensions; + } + + async scanSingleExtension(extensionLocation: URI, isBuiltin: boolean, language?: string): Promise { + await this.whenExtensionsReady(); + + const extensionPath = extensionLocation.scheme === Schemas.file ? extensionLocation.fsPath : null; + + if (!extensionPath) { + return null; + } + + const extension = await this._scanSingleExtension(extensionPath, isBuiltin, language ?? platform.language); + + if (!extension) { + return null; + } + + this._massageWhenConditions([extension]); + + return extension; + } + + private async _scanExtensions(profileLocation: URI, language: string, extensionDevelopmentPath: string[] | undefined, languagePackId: string | undefined): Promise { + await this._ensureLanguagePackIsInstalled(language, languagePackId); + + const [builtinExtensions, installedExtensions, developedExtensions] = await Promise.all([ + this._scanBuiltinExtensions(language), + this._scanInstalledExtensions(profileLocation, language), + this._scanDevelopedExtensions(language, extensionDevelopmentPath) + ]); + + return dedupExtensions(builtinExtensions, installedExtensions, developedExtensions, this._logService); + } + + private async _scanDevelopedExtensions(language: string, extensionDevelopmentPaths?: string[]): Promise { + if (extensionDevelopmentPaths) { + return (await Promise.all(extensionDevelopmentPaths.map(extensionDevelopmentPath => this._extensionsScannerService.scanOneOrMultipleExtensions(URI.file(resolve(extensionDevelopmentPath)), ExtensionType.User, { language })))) + .flat() + .map(e => toExtensionDescription(e, true)); + } + return []; + } + + private async _scanBuiltinExtensions(language: string): Promise { + const scannedExtensions = await this._extensionsScannerService.scanSystemExtensions({ language, useCache: true }); + return scannedExtensions.map(e => toExtensionDescription(e, false)); + } + + private async _scanInstalledExtensions(profileLocation: URI, language: string): Promise { + const scannedExtensions = await this._extensionsScannerService.scanUserExtensions({ profileLocation, language, useCache: true }); + return scannedExtensions.map(e => toExtensionDescription(e, false)); + } + + private async _scanSingleExtension(extensionPath: string, isBuiltin: boolean, language: string): Promise { + const extensionLocation = URI.file(resolve(extensionPath)); + const type = isBuiltin ? ExtensionType.System : ExtensionType.User; + const scannedExtension = await this._extensionsScannerService.scanExistingExtension(extensionLocation, type, { language }); + return scannedExtension ? toExtensionDescription(scannedExtension, false) : null; + } + + private async _ensureLanguagePackIsInstalled(language: string, languagePackId: string | undefined): Promise { + if ( + // No need to install language packs for the default language + language === platform.LANGUAGE_DEFAULT || + // The extension gallery service needs to be available + !this._extensionGalleryService.isEnabled() + ) { + return; + } + + try { + const installed = await this._languagePackService.getInstalledLanguages(); + if (installed.find(p => p.id === language)) { + this._logService.trace(`Language Pack ${language} is already installed. Skipping language pack installation.`); + return; + } + } catch (err) { + // We tried to see what is installed but failed. We can try installing anyway. + this._logService.error(err); + } + + if (!languagePackId) { + this._logService.trace(`No language pack id provided for language ${language}. Skipping language pack installation.`); + return; + } + + this._logService.trace(`Language Pack ${languagePackId} for language ${language} is not installed. It will be installed now.`); + try { + await this._extensionManagementCLI.installExtensions([languagePackId], [], { isMachineScoped: true }, true, { + log: (s) => this._logService.info(s), + error: (s) => this._logService.error(s) + }); + } catch (err) { + // We tried to install the language pack but failed. We can continue without it thus using the default language. + this._logService.error(err); + } + } + + private _massageWhenConditions(extensions: IExtensionDescription[]): void { + // Massage "when" conditions which mention `resourceScheme` + + interface WhenUser { when?: string } + + interface LocWhenUser { [loc: string]: WhenUser[] } + + const _mapResourceSchemeValue = (value: string, isRegex: boolean): string => { + // console.log(`_mapResourceSchemeValue: ${value}, ${isRegex}`); + return value.replace(/file/g, 'vscode-remote'); + }; + + const _mapResourceRegExpValue = (value: RegExp): RegExp => { + let flags = ''; + flags += value.global ? 'g' : ''; + flags += value.ignoreCase ? 'i' : ''; + flags += value.multiline ? 'm' : ''; + return new RegExp(_mapResourceSchemeValue(value.source, true), flags); + }; + + const _exprKeyMapper = new class implements IContextKeyExprMapper { + mapDefined(key: string): ContextKeyExpression { + return ContextKeyDefinedExpr.create(key); + } + mapNot(key: string): ContextKeyExpression { + return ContextKeyNotExpr.create(key); + } + mapEquals(key: string, value: any): ContextKeyExpression { + if (key === 'resourceScheme' && typeof value === 'string') { + return ContextKeyEqualsExpr.create(key, _mapResourceSchemeValue(value, false)); + } else { + return ContextKeyEqualsExpr.create(key, value); + } + } + mapNotEquals(key: string, value: any): ContextKeyExpression { + if (key === 'resourceScheme' && typeof value === 'string') { + return ContextKeyNotEqualsExpr.create(key, _mapResourceSchemeValue(value, false)); + } else { + return ContextKeyNotEqualsExpr.create(key, value); + } + } + mapGreater(key: string, value: any): ContextKeyExpression { + return ContextKeyGreaterExpr.create(key, value); + } + mapGreaterEquals(key: string, value: any): ContextKeyExpression { + return ContextKeyGreaterEqualsExpr.create(key, value); + } + mapSmaller(key: string, value: any): ContextKeyExpression { + return ContextKeySmallerExpr.create(key, value); + } + mapSmallerEquals(key: string, value: any): ContextKeyExpression { + return ContextKeySmallerEqualsExpr.create(key, value); + } + mapRegex(key: string, regexp: RegExp | null): ContextKeyRegexExpr { + if (key === 'resourceScheme' && regexp) { + return ContextKeyRegexExpr.create(key, _mapResourceRegExpValue(regexp)); + } else { + return ContextKeyRegexExpr.create(key, regexp); + } + } + mapIn(key: string, valueKey: string): ContextKeyInExpr { + return ContextKeyInExpr.create(key, valueKey); + } + mapNotIn(key: string, valueKey: string): ContextKeyNotInExpr { + return ContextKeyNotInExpr.create(key, valueKey); + } + }; + + const _massageWhenUser = (element: WhenUser) => { + if (!element || !element.when || !/resourceScheme/.test(element.when)) { + return; + } + + const expr = ContextKeyExpr.deserialize(element.when); + if (!expr) { + return; + } + + const massaged = expr.map(_exprKeyMapper); + element.when = massaged.serialize(); + }; + + const _massageWhenUserArr = (elements: WhenUser[] | WhenUser) => { + if (Array.isArray(elements)) { + for (const element of elements) { + _massageWhenUser(element); + } + } else { + _massageWhenUser(elements); + } + }; + + const _massageLocWhenUser = (target: LocWhenUser) => { + for (const loc in target) { + _massageWhenUserArr(target[loc]); + } + }; + + extensions.forEach((extension) => { + if (extension.contributes) { + if (extension.contributes.menus) { + _massageLocWhenUser(extension.contributes.menus); + } + if (extension.contributes.keybindings) { + _massageWhenUserArr(extension.contributes.keybindings); + } + if (extension.contributes.views) { + _massageLocWhenUser(extension.contributes.views); + } + } + }); + } +} + +export class RemoteExtensionsScannerChannel implements IServerChannel { + + constructor(private service: RemoteExtensionsScannerService, private getUriTransformer: (requestContext: any) => IURITransformer) { } + + listen(context: any, event: string): Event { + throw new Error('Invalid listen'); + } + + async call(context: any, command: string, args?: any): Promise { + const uriTransformer = this.getUriTransformer(context); + switch (command) { + case 'whenExtensionsReady': return this.service.whenExtensionsReady(); + case 'scanExtensions': { + const language = args[0]; + const profileLocation = args[1] ? URI.revive(uriTransformer.transformIncoming(args[1])) : undefined; + const extensionDevelopmentPath = Array.isArray(args[2]) ? args[2].map(u => URI.revive(uriTransformer.transformIncoming(u))) : undefined; + const languagePackId: string | undefined = args[3]; + const extensions = await this.service.scanExtensions(language, profileLocation, extensionDevelopmentPath, languagePackId); + return extensions.map(extension => transformOutgoingURIs(extension, uriTransformer)); + } + case 'scanSingleExtension': { + const extension = await this.service.scanSingleExtension(URI.revive(uriTransformer.transformIncoming(args[0])), args[1], args[2]); + return extension ? transformOutgoingURIs(extension, uriTransformer) : null; + } + } + throw new Error('Invalid call'); + } +} diff --git a/src/vs/server/node/serverServices.ts b/src/vs/server/node/serverServices.ts index c51c166cf51..408b37adf23 100644 --- a/src/vs/server/node/serverServices.ts +++ b/src/vs/server/node/serverServices.ts @@ -79,6 +79,9 @@ import { ExtensionsProfileScannerService } from 'vs/platform/extensionManagement import { LogService } from 'vs/platform/log/common/logService'; import { LoggerChannel } from 'vs/platform/log/common/logIpc'; import { localize } from 'vs/nls'; +import { RemoteExtensionsScannerChannel, RemoteExtensionsScannerService } from 'vs/server/node/remoteExtensionsScanner'; +import { RemoteExtensionsScannerChannelName } from 'vs/platform/remote/common/remoteExtensionsScanner'; +import { RemoteUserDataProfilesServiceChannel } from 'vs/platform/userDataProfile/common/userDataProfileIpc'; const eventPrefix = 'monacoworkbench'; @@ -130,6 +133,7 @@ export async function setupServerServices(connectionToken: ServerConnectionToken // User Data Profiles const userDataProfilesService = new ServerUserDataProfilesService(uriIdentityService, environmentService, fileService, logService); services.set(IUserDataProfilesService, userDataProfilesService); + socketServer.registerChannel('userDataProfiles', new RemoteUserDataProfilesServiceChannel(userDataProfilesService, (ctx: RemoteAgentConnectionContext) => getUriTransformer(ctx.remoteAuthority))); // Initialize const [, , machineId] = await Promise.all([ @@ -205,7 +209,9 @@ export async function setupServerServices(connectionToken: ServerConnectionToken instantiationService.invokeFunction(accessor => { const extensionManagementService = accessor.get(INativeServerExtensionManagementService); const extensionsScannerService = accessor.get(IExtensionsScannerService); - const remoteExtensionEnvironmentChannel = new RemoteAgentEnvironmentChannel(connectionToken, environmentService, userDataProfilesService, instantiationService.createInstance(ExtensionManagementCLI), logService, extensionHostStatusService, extensionsScannerService); + const extensionGalleryService = accessor.get(IExtensionGalleryService); + const languagePackService = accessor.get(ILanguagePackService); + const remoteExtensionEnvironmentChannel = new RemoteAgentEnvironmentChannel(connectionToken, environmentService, userDataProfilesService, extensionHostStatusService); socketServer.registerChannel('remoteextensionsenvironment', remoteExtensionEnvironmentChannel); const telemetryChannel = new ServerTelemetryChannel(accessor.get(IServerTelemetryService), oneDsAppender); @@ -213,6 +219,9 @@ export async function setupServerServices(connectionToken: ServerConnectionToken socketServer.registerChannel(REMOTE_TERMINAL_CHANNEL_NAME, new RemoteTerminalChannel(environmentService, logService, ptyService, productService, extensionManagementService, configurationService)); + const remoteExtensionsScanner = new RemoteExtensionsScannerService(instantiationService.createInstance(ExtensionManagementCLI), environmentService, userDataProfilesService, extensionsScannerService, logService, extensionGalleryService, languagePackService); + socketServer.registerChannel(RemoteExtensionsScannerChannelName, new RemoteExtensionsScannerChannel(remoteExtensionsScanner, (ctx: RemoteAgentConnectionContext) => getUriTransformer(ctx.remoteAuthority))); + const remoteFileSystemChannel = new RemoteAgentFileSystemProviderChannel(logService, environmentService); socketServer.registerChannel(REMOTE_FILE_SYSTEM_CHANNEL_NAME, remoteFileSystemChannel); diff --git a/src/vs/workbench/api/browser/mainThreadLocalization.ts b/src/vs/workbench/api/browser/mainThreadLocalization.ts index cc532e404ea..af85618a64c 100644 --- a/src/vs/workbench/api/browser/mainThreadLocalization.ts +++ b/src/vs/workbench/api/browser/mainThreadLocalization.ts @@ -21,9 +21,9 @@ export class MainThreadLocalization extends Disposable implements MainThreadLoca super(); } - async $fetchBuiltInBundleUri(id: string): Promise { + async $fetchBuiltInBundleUri(id: string, language: string): Promise { try { - const uri = await this.languagePackService.getBuiltInExtensionTranslationsUri(id); + const uri = await this.languagePackService.getBuiltInExtensionTranslationsUri(id, language); return uri; } catch (e) { return undefined; diff --git a/src/vs/workbench/api/browser/mainThreadSCM.ts b/src/vs/workbench/api/browser/mainThreadSCM.ts index ea613cde683..f75b82bee9f 100644 --- a/src/vs/workbench/api/browser/mainThreadSCM.ts +++ b/src/vs/workbench/api/browser/mainThreadSCM.ts @@ -158,7 +158,11 @@ class MainThreadSCMProvider implements ISCMProvider, QuickDiffProvider { } if (features.hasQuickDiffProvider && !this._quickDiff) { - this._quickDiff = this._quickDiffService.addQuickDiffProvider(this); + this._quickDiff = this._quickDiffService.addQuickDiffProvider({ + label: features.quickDiffLabel ?? this.label, + rootUri: this.rootUri, + getOriginalResource: (uri: URI) => this.getOriginalResource(uri) + }); } else if (features.hasQuickDiffProvider === false && this._quickDiff) { this._quickDiff.dispose(); this._quickDiff = undefined; diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 27edca9fa34..8886bf9d058 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -670,7 +670,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I return >extHostMessageService.showMessage(extension, Severity.Error, message, rest[0], >rest.slice(1)); }, showQuickPick(items: any, options?: vscode.QuickPickOptions, token?: vscode.CancellationToken): any { - return extHostQuickOpen.showQuickPick(items, options, token); + return extHostQuickOpen.showQuickPick(extension, items, options, token); }, showWorkspaceFolderPick(options?: vscode.WorkspaceFolderPickOptions) { return extHostQuickOpen.showWorkspaceFolderPick(options); diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 414c984240f..c61347eb893 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -1179,6 +1179,7 @@ export interface MainThreadExtensionServiceShape extends IDisposable { export interface SCMProviderFeatures { hasQuickDiffProvider?: boolean; + quickDiffLabel?: string; count?: number; commitTemplate?: string; acceptInputCommand?: languages.Command; @@ -2193,7 +2194,7 @@ export interface MainThreadThemingShape extends IDisposable { } export interface MainThreadLocalizationShape extends IDisposable { - $fetchBuiltInBundleUri(id: string): Promise; + $fetchBuiltInBundleUri(id: string, language: string): Promise; $fetchBundleContents(uriComponents: UriComponents): Promise; } diff --git a/src/vs/workbench/api/common/extHostLocalizationService.ts b/src/vs/workbench/api/common/extHostLocalizationService.ts index 7acfa075c65..6ed204b58dd 100644 --- a/src/vs/workbench/api/common/extHostLocalizationService.ts +++ b/src/vs/workbench/api/common/extHostLocalizationService.ts @@ -95,7 +95,7 @@ export class ExtHostLocalizationService implements ExtHostLocalizationShape { private async getBundleLocation(extension: IExtensionDescription): Promise { if (extension.isBuiltin) { - const uri = await this._proxy.$fetchBuiltInBundleUri(extension.identifier.value); + const uri = await this._proxy.$fetchBuiltInBundleUri(extension.identifier.value, this.currentLanguage); return URI.revive(uri); } diff --git a/src/vs/workbench/api/common/extHostQuickDiff.ts b/src/vs/workbench/api/common/extHostQuickDiff.ts index 68cce87bc2d..04e7c04a16c 100644 --- a/src/vs/workbench/api/common/extHostQuickDiff.ts +++ b/src/vs/workbench/api/common/extHostQuickDiff.ts @@ -41,7 +41,10 @@ export class ExtHostQuickDiff implements ExtHostQuickDiffShape { this.providers.set(handle, quickDiffProvider); this.proxy.$registerQuickDiffProvider(handle, DocumentSelector.from(selector, this.uriTransformer), label, rootUri); return { - dispose: () => this.providers.delete(handle) + dispose: () => { + this.proxy.$unregisterQuickDiffProvider(handle); + this.providers.delete(handle); + } }; } } diff --git a/src/vs/workbench/api/common/extHostQuickOpen.ts b/src/vs/workbench/api/common/extHostQuickOpen.ts index 4ae22adf430..558c61f22fb 100644 --- a/src/vs/workbench/api/common/extHostQuickOpen.ts +++ b/src/vs/workbench/api/common/extHostQuickOpen.ts @@ -17,14 +17,16 @@ import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensio import { coalesce } from 'vs/base/common/arrays'; import Severity from 'vs/base/common/severity'; import { ThemeIcon as ThemeIconUtils } from 'vs/base/common/themables'; +import { isProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; +import { MarkdownString } from 'vs/workbench/api/common/extHostTypeConverters'; export type Item = string | QuickPickItem; export interface ExtHostQuickOpen { - showQuickPick(itemsOrItemsPromise: QuickPickItem[] | Promise, options: QuickPickOptions & { canPickMany: true }, token?: CancellationToken): Promise; - showQuickPick(itemsOrItemsPromise: string[] | Promise, options?: QuickPickOptions, token?: CancellationToken): Promise; - showQuickPick(itemsOrItemsPromise: QuickPickItem[] | Promise, options?: QuickPickOptions, token?: CancellationToken): Promise; - showQuickPick(itemsOrItemsPromise: Item[] | Promise, options?: QuickPickOptions, token?: CancellationToken): Promise; + showQuickPick(extension: IExtensionDescription, itemsOrItemsPromise: QuickPickItem[] | Promise, options: QuickPickOptions & { canPickMany: true }, token?: CancellationToken): Promise; + showQuickPick(extension: IExtensionDescription, itemsOrItemsPromise: string[] | Promise, options?: QuickPickOptions, token?: CancellationToken): Promise; + showQuickPick(extension: IExtensionDescription, itemsOrItemsPromise: QuickPickItem[] | Promise, options?: QuickPickOptions, token?: CancellationToken): Promise; + showQuickPick(extension: IExtensionDescription, itemsOrItemsPromise: Item[] | Promise, options?: QuickPickOptions, token?: CancellationToken): Promise; showInput(options?: InputBoxOptions, token?: CancellationToken): Promise; @@ -55,11 +57,10 @@ export function createExtHostQuickOpen(mainContext: IMainContext, workspace: IEx this._commands = commands; } - showQuickPick(itemsOrItemsPromise: QuickPickItem[] | Promise, options: QuickPickOptions & { canPickMany: true }, token?: CancellationToken): Promise; - showQuickPick(itemsOrItemsPromise: string[] | Promise, options?: QuickPickOptions, token?: CancellationToken): Promise; - showQuickPick(itemsOrItemsPromise: QuickPickItem[] | Promise, options?: QuickPickOptions, token?: CancellationToken): Promise; - showQuickPick(itemsOrItemsPromise: Item[] | Promise, options?: QuickPickOptions, token: CancellationToken = CancellationToken.None): Promise { - + showQuickPick(extension: IExtensionDescription, itemsOrItemsPromise: QuickPickItem[] | Promise, options: QuickPickOptions & { canPickMany: true }, token?: CancellationToken): Promise; + showQuickPick(extension: IExtensionDescription, itemsOrItemsPromise: string[] | Promise, options?: QuickPickOptions, token?: CancellationToken): Promise; + showQuickPick(extension: IExtensionDescription, itemsOrItemsPromise: QuickPickItem[] | Promise, options?: QuickPickOptions, token?: CancellationToken): Promise; + showQuickPick(extension: IExtensionDescription, itemsOrItemsPromise: Item[] | Promise, options?: QuickPickOptions, token: CancellationToken = CancellationToken.None): Promise { // clear state from last invocation this._onDidSelectItem = undefined; @@ -84,6 +85,8 @@ export function createExtHostQuickOpen(mainContext: IMainContext, workspace: IEx return undefined; } + const allowedTooltips = isProposedApiEnabled(extension, 'quickPickItemTooltip'); + return itemsPromise.then(items => { const pickItems: TransferQuickPickItemOrSeparator[] = []; @@ -94,12 +97,16 @@ export function createExtHostQuickOpen(mainContext: IMainContext, workspace: IEx } else if (item.kind === QuickPickItemKind.Separator) { pickItems.push({ type: 'separator', label: item.label }); } else { + if (item.tooltip && !allowedTooltips) { + console.warn(`Extension '${extension.identifier.value}' uses a tooltip which is proposed API that is only available when running out of dev or with the following command line switch: --enable-proposed-api ${extension.identifier.value}`); + } pickItems.push({ label: item.label, description: item.description, detail: item.detail, picked: item.picked, alwaysShow: item.alwaysShow, + tooltip: allowedTooltips ? MarkdownString.fromStrict(item.tooltip) : undefined, handle }); } @@ -535,7 +542,7 @@ export function createExtHostQuickOpen(mainContext: IMainContext, workspace: IEx private readonly _onDidChangeSelectionEmitter = new Emitter(); private readonly _onDidTriggerItemButtonEmitter = new Emitter>(); - constructor(extension: IExtensionDescription, onDispose: () => void) { + constructor(private extension: IExtensionDescription, onDispose: () => void) { super(extension.identifier, onDispose); this._disposables.push( this._onDidChangeActiveEmitter, @@ -558,12 +565,16 @@ export function createExtHostQuickOpen(mainContext: IMainContext, workspace: IEx this._itemsToHandles.set(item, i); }); + const allowedTooltips = isProposedApiEnabled(this.extension, 'quickPickItemTooltip'); const pickItems: TransferQuickPickItemOrSeparator[] = []; for (let handle = 0; handle < items.length; handle++) { const item = items[handle]; if (item.kind === QuickPickItemKind.Separator) { pickItems.push({ type: 'separator', label: item.label }); } else { + if (item.tooltip && !allowedTooltips) { + console.warn(`Extension '${this.extension.identifier.value}' uses a tooltip which is proposed API that is only available when running out of dev or with the following command line switch: --enable-proposed-api ${this.extension.identifier.value}`); + } pickItems.push({ handle, label: item.label, @@ -571,6 +582,7 @@ export function createExtHostQuickOpen(mainContext: IMainContext, workspace: IEx detail: item.detail, picked: item.picked, alwaysShow: item.alwaysShow, + tooltip: allowedTooltips ? MarkdownString.fromStrict(item.tooltip) : undefined, buttons: item.buttons?.map((button, i) => { return { ...getIconPathOrClass(button), diff --git a/src/vs/workbench/api/common/extHostSCM.ts b/src/vs/workbench/api/common/extHostSCM.ts index 147ea6be023..86426a0df86 100644 --- a/src/vs/workbench/api/common/extHostSCM.ts +++ b/src/vs/workbench/api/common/extHostSCM.ts @@ -488,7 +488,10 @@ class ExtHostSourceControl implements vscode.SourceControl { set quickDiffProvider(quickDiffProvider: vscode.QuickDiffProvider | undefined) { this._quickDiffProvider = quickDiffProvider; - this.#proxy.$updateSourceControl(this.handle, { hasQuickDiffProvider: !!quickDiffProvider }); + if (quickDiffProvider?.label) { + checkProposedApiEnabled(this._extension, 'quickDiffProvider'); + } + this.#proxy.$updateSourceControl(this.handle, { hasQuickDiffProvider: !!quickDiffProvider, quickDiffLabel: quickDiffProvider?.label }); } private _commitTemplate: string | undefined = undefined; diff --git a/src/vs/workbench/api/test/browser/mainThreadDocumentsAndEditors.test.ts b/src/vs/workbench/api/test/browser/mainThreadDocumentsAndEditors.test.ts index bb18933c165..14ef427ac59 100644 --- a/src/vs/workbench/api/test/browser/mainThreadDocumentsAndEditors.test.ts +++ b/src/vs/workbench/api/test/browser/mainThreadDocumentsAndEditors.test.ts @@ -20,7 +20,6 @@ import { ServiceCollection } from 'vs/platform/instantiation/common/serviceColle import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { IFileService } from 'vs/platform/files/common/files'; import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService'; -import { NullLogService } from 'vs/platform/log/common/log'; import { UndoRedoService } from 'vs/platform/undoRedo/common/undoRedoService'; import { TestDialogService } from 'vs/platform/dialogs/test/common/testDialogService'; import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService'; @@ -32,8 +31,6 @@ import { TestLanguageConfigurationService } from 'vs/editor/test/common/modes/te import { TextModel } from 'vs/editor/common/model/textModel'; import { LanguageService } from 'vs/editor/common/services/languageService'; import { DisposableStore } from 'vs/base/common/lifecycle'; -import { LanguageFeatureDebounceService } from 'vs/editor/common/services/languageFeatureDebounce'; -import { LanguageFeaturesService } from 'vs/editor/common/services/languageFeaturesService'; suite('MainThreadDocumentsAndEditors', () => { @@ -63,17 +60,12 @@ suite('MainThreadDocumentsAndEditors', () => { const notificationService = new TestNotificationService(); const undoRedoService = new UndoRedoService(dialogService, notificationService); const themeService = new TestThemeService(); - const logService = new NullLogService(); modelService = new ModelService( configService, new TestTextResourcePropertiesService(configService), - themeService, - new NullLogService(), undoRedoService, disposables.add(new LanguageService()), new TestLanguageConfigurationService(), - new LanguageFeatureDebounceService(logService), - new LanguageFeaturesService() ); codeEditorService = new TestCodeEditorService(themeService); textFileService = new class extends mock() { diff --git a/src/vs/workbench/api/test/browser/mainThreadEditors.test.ts b/src/vs/workbench/api/test/browser/mainThreadEditors.test.ts index 7f9da848d8c..7a9bf86bce9 100644 --- a/src/vs/workbench/api/test/browser/mainThreadEditors.test.ts +++ b/src/vs/workbench/api/test/browser/mainThreadEditors.test.ts @@ -52,8 +52,6 @@ import { IEnvironmentService } from 'vs/platform/environment/common/environment' import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite'; import { TestLanguageConfigurationService } from 'vs/editor/test/common/modes/testLanguageConfigurationService'; import { LanguageService } from 'vs/editor/common/services/languageService'; -import { LanguageFeatureDebounceService } from 'vs/editor/common/services/languageFeatureDebounce'; -import { LanguageFeaturesService } from 'vs/editor/common/services/languageFeaturesService'; import { MainThreadBulkEdits } from 'vs/workbench/api/browser/mainThreadBulkEdits'; import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { UriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentityService'; @@ -86,17 +84,12 @@ suite('MainThreadEditors', () => { const notificationService = new TestNotificationService(); const undoRedoService = new UndoRedoService(dialogService, notificationService); const themeService = new TestThemeService(); - const logService = new NullLogService(); modelService = new ModelService( configService, new TestTextResourcePropertiesService(configService), - themeService, - logService, undoRedoService, disposables.add(new LanguageService()), new TestLanguageConfigurationService(), - new LanguageFeatureDebounceService(logService), - new LanguageFeaturesService() ); const services = new ServiceCollection(); diff --git a/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts b/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts index 25e258e3f31..1aeaf05b8a8 100644 --- a/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts +++ b/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts @@ -5,10 +5,11 @@ import 'vs/css!./media/activityaction'; import { localize } from 'vs/nls'; -import { EventType, addDisposableListener, EventHelper } from 'vs/base/browser/dom'; +import { EventType, addDisposableListener, EventHelper, append, $, clearNode, hide, show } from 'vs/base/browser/dom'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { EventType as TouchEventType, GestureEvent } from 'vs/base/browser/touch'; import { Action, IAction, Separator, SubmenuAction, toAction } from 'vs/base/common/actions'; +import { Event } from 'vs/base/common/event'; import { KeyCode } from 'vs/base/common/keyCodes'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { IMenuService, MenuId, IMenu, registerAction2, Action2, IAction2Options } from 'vs/platform/actions/common/actions'; @@ -344,6 +345,9 @@ export interface IProfileActivity extends IActivity { export class GlobalActivityActionViewItem extends MenuActivityActionViewItem { + private profileBadge: HTMLElement | undefined; + private profileBadgeContent: HTMLElement | undefined; + constructor( action: ActivityAction, contextMenuActionsProvider: () => IAction[], @@ -360,6 +364,31 @@ export class GlobalActivityActionViewItem extends MenuActivityActionViewItem { @IKeybindingService keybindingService: IKeybindingService, ) { super(MenuId.GlobalActivity, action, contextMenuActionsProvider, true, colors, activityHoverOptions, themeService, hoverService, menuService, contextMenuService, contextKeyService, configurationService, environmentService, keybindingService); + this._register(Event.any(this.userDataProfileService.onDidUpdateCurrentProfile, this.userDataProfileService.onDidChangeCurrentProfile)(() => this.updateProfileBadge())); + } + + override render(container: HTMLElement): void { + super.render(container); + + this.profileBadge = append(container, $('.profile-badge')); + this.profileBadgeContent = append(this.profileBadge, $('.profile-badge-content')); + this.updateProfileBadge(); + } + + protected updateProfileBadge(): void { + if (!this.profileBadge || !this.profileBadgeContent) { + return; + } + + clearNode(this.profileBadgeContent); + hide(this.profileBadge); + + if (this.userDataProfileService.currentProfile.isDefault) { + return; + } + + this.profileBadgeContent.textContent = this.userDataProfileService.currentProfile.name.substring(0, 2).toUpperCase(); + show(this.profileBadge); } protected override computeTitle(): string { diff --git a/src/vs/workbench/browser/parts/activitybar/media/activityaction.css b/src/vs/workbench/browser/parts/activitybar/media/activityaction.css index 5f52051112b..83cc1b469fa 100644 --- a/src/vs/workbench/browser/parts/activitybar/media/activityaction.css +++ b/src/vs/workbench/browser/parts/activitybar/media/activityaction.css @@ -128,10 +128,10 @@ outline: none; } +.monaco-workbench .activitybar > .content :not(.monaco-menu) > .monaco-action-bar .profile-badge, .monaco-workbench .activitybar > .content :not(.monaco-menu) > .monaco-action-bar .active-item-indicator, .monaco-workbench .activitybar > .content :not(.monaco-menu) > .monaco-action-bar .badge { position: absolute; - z-index: 1; top: 0; bottom: 0; margin: auto; @@ -141,6 +141,15 @@ height: 100%; } +.monaco-workbench .activitybar > .content :not(.monaco-menu) > .monaco-action-bar .active-item-indicator, +.monaco-workbench .activitybar > .content :not(.monaco-menu) > .monaco-action-bar .badge { + z-index: 2; +} + +.monaco-workbench .activitybar > .content :not(.monaco-menu) > .monaco-action-bar .profile-badge { + z-index: 1; +} + .monaco-workbench .activitybar > .content :not(.monaco-menu) > .monaco-action-bar .active-item-indicator { pointer-events: none; } @@ -163,6 +172,25 @@ text-align: center; } +.monaco-workbench .activitybar > .content :not(.monaco-menu) > .monaco-action-bar .profile-badge .profile-badge-content { + position: absolute; + font-weight: 600; + font-size: 11px; + line-height: 10px; + top: 28px; + right: 24px; + padding: 1px; + border-radius: 3px; + background-color: var(--vscode-activityBar-background); + color: var(--vscode-activityBar-inactiveForeground); + border: 1px solid; +} + +.monaco-workbench .activitybar > .content :not(.monaco-menu) > .monaco-action-bar .action-item:active .profile-badge-content, +.monaco-workbench .activitybar > .content :not(.monaco-menu) > .monaco-action-bar .action-item:focus .profile-badge-content, +.monaco-workbench .activitybar > .content :not(.monaco-menu) > .monaco-action-bar .action-item:hover .profile-badge-content { + color: var(--vscode-activityBar-foreground); +} .monaco-workbench .activitybar > .content :not(.monaco-menu) > .monaco-action-bar .badge .codicon.badge-content { font-size: 12px; @@ -178,6 +206,7 @@ /* Right aligned */ +.monaco-workbench .activitybar.right>.content :not(.monaco-menu)>.monaco-action-bar .profile-badge, .monaco-workbench .activitybar.right > .content :not(.monaco-menu) > .monaco-action-bar .badge { left: auto; right: 0; diff --git a/src/vs/workbench/browser/parts/editor/editorStatus.ts b/src/vs/workbench/browser/parts/editor/editorStatus.ts index 746dadb68a5..551dde7791f 100644 --- a/src/vs/workbench/browser/parts/editor/editorStatus.ts +++ b/src/vs/workbench/browser/parts/editor/editorStatus.ts @@ -318,7 +318,7 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { this._register(this.editorService.onDidActiveEditorChange(() => this.updateStatusBar())); this._register(this.textFileService.untitled.onDidChangeEncoding(model => this.onResourceEncodingChange(model.resource))); this._register(this.textFileService.files.onDidChangeEncoding(model => this.onResourceEncodingChange((model.resource)))); - this._register(TabFocus.onDidChangeTabFocus(() => this.onTabFocusModeChange())); + this._register(Event.runAndSubscribe(TabFocus.onDidChangeTabFocus, () => this.onTabFocusModeChange())); } private registerCommands(): void { diff --git a/src/vs/workbench/browser/web.main.ts b/src/vs/workbench/browser/web.main.ts index e5c6c33e5dd..9ff45090e6a 100644 --- a/src/vs/workbench/browser/web.main.ts +++ b/src/vs/workbench/browser/web.main.ts @@ -268,10 +268,6 @@ export class BrowserMain extends Disposable { // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - // Remote Agent - const remoteAgentService = this._register(new RemoteAgentService(this.configuration.webSocketFactory, environmentService, productService, remoteAuthorityResolverService, signService, logService)); - serviceCollection.set(IRemoteAgentService, remoteAgentService); - // Files const fileService = this._register(new FileService(logService)); serviceCollection.set(IWorkbenchFileService, fileService); @@ -280,8 +276,6 @@ export class BrowserMain extends Disposable { const loggerService = new FileLoggerService(logLevel, fileService); serviceCollection.set(ILoggerService, loggerService); - await this.registerFileSystemProviders(environmentService, fileService, remoteAgentService, bufferLogger, logService, loggerService, logsPath); - // URI Identity const uriIdentityService = new UriIdentityService(fileService); serviceCollection.set(IUriIdentityService, uriIdentityService); @@ -289,15 +283,17 @@ export class BrowserMain extends Disposable { // User Data Profiles const userDataProfilesService = new BrowserUserDataProfilesService(environmentService, fileService, uriIdentityService, logService); serviceCollection.set(IUserDataProfilesService, userDataProfilesService); - if (environmentService.remoteAuthority) { - // Always Disabled in web with remote connection - userDataProfilesService.setEnablement(false); - } const currentProfile = userDataProfilesService.getProfileForWorkspace(workspace) ?? userDataProfilesService.defaultProfile; const userDataProfileService = new UserDataProfileService(currentProfile, userDataProfilesService); serviceCollection.set(IUserDataProfileService, userDataProfileService); + // Remote Agent + const remoteAgentService = this._register(new RemoteAgentService(this.configuration.webSocketFactory, userDataProfileService, environmentService, productService, remoteAuthorityResolverService, signService, logService)); + serviceCollection.set(IRemoteAgentService, remoteAgentService); + + await this.registerFileSystemProviders(environmentService, fileService, remoteAgentService, bufferLogger, logService, loggerService, logsPath); + // Long running services (workspace, config, storage) const [configurationService, storageService] = await Promise.all([ this.createWorkspaceService(workspace, environmentService, userDataProfileService, userDataProfilesService, fileService, remoteAgentService, uriIdentityService, logService).then(service => { diff --git a/src/vs/workbench/common/editor/editorGroupModel.ts b/src/vs/workbench/common/editor/editorGroupModel.ts index 4384de2cc62..a65dc729932 100644 --- a/src/vs/workbench/common/editor/editorGroupModel.ts +++ b/src/vs/workbench/common/editor/editorGroupModel.ts @@ -551,6 +551,7 @@ export class EditorGroupModel extends Disposable { } const editor = this.editors[index]; + const sticky = this.sticky; // Adjust sticky index: editor moved out of sticky state into unsticky state if (this.isSticky(index) && toIndex > this.sticky) { @@ -566,7 +567,7 @@ export class EditorGroupModel extends Disposable { this.editors.splice(index, 1); this.editors.splice(toIndex, 0, editor); - // Event + // Move Event const event: IGroupEditorMoveEvent = { kind: GroupModelChangeKind.EDITOR_MOVE, editor, @@ -575,6 +576,16 @@ export class EditorGroupModel extends Disposable { }; this._onDidModelChange.fire(event); + // Sticky Event (if sticky changed as part of the move) + if (sticky !== this.sticky) { + const event: IGroupEditorChangeEvent = { + kind: GroupModelChangeKind.EDITOR_STICKY, + editor, + editorIndex: toIndex + }; + this._onDidModelChange.fire(event); + } + return editor; } diff --git a/src/vs/workbench/contrib/audioCues/browser/audioCues.contribution.ts b/src/vs/workbench/contrib/audioCues/browser/audioCues.contribution.ts index 1f345057700..61153542d54 100644 --- a/src/vs/workbench/contrib/audioCues/browser/audioCues.contribution.ts +++ b/src/vs/workbench/contrib/audioCues/browser/audioCues.contribution.ts @@ -89,6 +89,10 @@ Registry.as(ConfigurationExtensions.Configuration).regis 'description': localize('audioCues.taskFailed', "Plays a sound when a task fails (non-zero exit code)."), ...audioCueFeatureBase, }, + 'audioCues.terminalCommandFailed': { + 'description': localize('audioCues.terminalCommandFailed', "Plays a sound when a terminal command fails (non-zero exit code)."), + ...audioCueFeatureBase, + }, 'audioCues.terminalQuickFix': { 'description': localize('audioCues.terminalQuickFix', "Plays a sound when terminal Quick Fixes are available."), ...audioCueFeatureBase, diff --git a/src/vs/workbench/contrib/codeEditor/browser/codeEditor.contribution.ts b/src/vs/workbench/contrib/codeEditor/browser/codeEditor.contribution.ts index d10833a37f5..3e332198bf0 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/codeEditor.contribution.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/codeEditor.contribution.ts @@ -6,6 +6,7 @@ import './menuPreventer'; import './accessibility/accessibility'; import './diffEditorHelper'; +import './editorFeatures'; import './editorSettingsMigration'; import './inspectKeybindings'; import './largeFileOptimizations'; diff --git a/src/vs/workbench/contrib/codeEditor/browser/editorFeatures.ts b/src/vs/workbench/contrib/codeEditor/browser/editorFeatures.ts new file mode 100644 index 00000000000..e51906b362e --- /dev/null +++ b/src/vs/workbench/contrib/codeEditor/browser/editorFeatures.ts @@ -0,0 +1,55 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { onUnexpectedError } from 'vs/base/common/errors'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; +import { getEditorFeatures } from 'vs/editor/common/editorFeatures'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { IWorkbenchContribution, IWorkbenchContributionsRegistry, Extensions } from 'vs/workbench/common/contributions'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; +import { IDisposable } from 'xterm'; + +class EditorFeaturesInstantiator extends Disposable implements IWorkbenchContribution { + + private _instantiated = false; + + constructor( + @ICodeEditorService codeEditorService: ICodeEditorService, + @IInstantiationService private readonly _instantiationService: IInstantiationService + ) { + super(); + + this._register(codeEditorService.onWillCreateCodeEditor(() => this._instantiate())); + this._register(codeEditorService.onWillCreateDiffEditor(() => this._instantiate())); + if (codeEditorService.listCodeEditors().length > 0 || codeEditorService.listDiffEditors().length > 0) { + this._instantiate(); + } + } + + private _instantiate(): void { + if (this._instantiated) { + return; + } + this._instantiated = true; + + // Instantiate all editor features + const editorFeatures = getEditorFeatures(); + for (const feature of editorFeatures) { + try { + const instance = this._instantiationService.createInstance(feature); + if (typeof (instance).dispose === 'function') { + this._register((instance)); + } + } catch (err) { + onUnexpectedError(err); + } + } + } +} + +const workbenchRegistry = Registry.as(Extensions.Workbench); +workbenchRegistry.registerWorkbenchContribution(EditorFeaturesInstantiator, LifecyclePhase.Ready); diff --git a/src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.ts b/src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.ts index 7a9e3970e89..18ced65b47d 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.ts @@ -28,7 +28,7 @@ import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { ColorThemeData, TokenStyleDefinitions, TokenStyleDefinition, TextMateThemingRuleDefinitions } from 'vs/workbench/services/themes/common/colorThemeData'; import { SemanticTokenRule, TokenStyleData, TokenStyle } from 'vs/platform/theme/common/tokenClassificationRegistry'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { SEMANTIC_HIGHLIGHTING_SETTING_ID, IEditorSemanticHighlightingOptions } from 'vs/editor/common/services/modelService'; +import { SEMANTIC_HIGHLIGHTING_SETTING_ID, IEditorSemanticHighlightingOptions } from 'vs/editor/contrib/semanticTokens/common/semanticTokensConfig'; import { Schemas } from 'vs/base/common/network'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; diff --git a/src/vs/workbench/contrib/comments/browser/commentGlyphWidget.ts b/src/vs/workbench/contrib/comments/browser/commentGlyphWidget.ts index 38c9dbde21b..76d0a4a7b95 100644 --- a/src/vs/workbench/contrib/comments/browser/commentGlyphWidget.ts +++ b/src/vs/workbench/contrib/comments/browser/commentGlyphWidget.ts @@ -11,6 +11,7 @@ import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; import { darken, editorBackground, editorForeground, listInactiveSelectionBackground, opaque, registerColor } from 'vs/platform/theme/common/colorRegistry'; import { themeColorFromId } from 'vs/platform/theme/common/themeService'; import { IEditorDecorationsCollection } from 'vs/editor/common/editorCommon'; +import { CommentThreadState } from 'vs/editor/common/languages'; export const overviewRulerCommentingRangeForeground = registerColor('editorGutter.commentRangeForeground', { dark: opaque(listInactiveSelectionBackground, editorBackground), light: darken(opaque(listInactiveSelectionBackground, editorBackground), .05), hcDark: Color.white, hcLight: Color.black }, nls.localize('editorGutterCommentRangeForeground', 'Editor gutter decoration color for commenting ranges. This color should be opaque.')); registerColor('editorGutter.commentGlyphForground', { dark: editorForeground, light: editorForeground, hcDark: Color.black, hcLight: Color.white }, nls.localize('editorGutterCommentGlyphForeground', 'Editor gutter decoration color for commenting glyphs.')); @@ -19,6 +20,7 @@ export class CommentGlyphWidget { public static description = 'comment-glyph-widget'; private _lineNumber!: number; private _editor: ICodeEditor; + private _threadState: CommentThreadState | undefined; private readonly _commentsDecorations: IEditorDecorationsCollection; private _commentsOptions: ModelDecorationOptions; @@ -38,18 +40,25 @@ export class CommentGlyphWidget { position: OverviewRulerLane.Center }, collapseOnReplaceEdit: true, - linesDecorationsClassName: `comment-range-glyph comment-thread` + linesDecorationsClassName: `comment-range-glyph comment-thread${this._threadState === CommentThreadState.Unresolved ? '-unresolved' : ''}` }; return ModelDecorationOptions.createDynamic(decorationOptions); } - setLineNumber(lineNumber: number): void { - this._lineNumber = lineNumber; + setThreadState(state: CommentThreadState | undefined): void { + if (this._threadState !== state) { + this._threadState = state; + this._commentsOptions = this.createDecorationOptions(); + this._updateDecorations(); + } + } + + private _updateDecorations(): void { const commentsDecorations = [{ range: { - startLineNumber: lineNumber, startColumn: 1, - endLineNumber: lineNumber, endColumn: 1 + startLineNumber: this._lineNumber, startColumn: 1, + endLineNumber: this._lineNumber, endColumn: 1 }, options: this._commentsOptions }]; @@ -57,6 +66,11 @@ export class CommentGlyphWidget { this._commentsDecorations.set(commentsDecorations); } + setLineNumber(lineNumber: number): void { + this._lineNumber = lineNumber; + this._updateDecorations(); + } + getPosition(): IContentWidgetPosition { const range = (this._commentsDecorations.length > 0 ? this._commentsDecorations.getRange(0) : null); diff --git a/src/vs/workbench/contrib/comments/browser/commentNode.ts b/src/vs/workbench/contrib/comments/browser/commentNode.ts index 87a32fe2123..f8414f2b54e 100644 --- a/src/vs/workbench/contrib/comments/browser/commentNode.ts +++ b/src/vs/workbench/contrib/comments/browser/commentNode.ts @@ -44,6 +44,7 @@ import { CommentMenus } from 'vs/workbench/contrib/comments/browser/commentMenus import { Scrollable, ScrollbarVisibility } from 'vs/base/common/scrollable'; import { SmoothScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; import { DomEmitter } from 'vs/base/browser/event'; +import { CommentContextKeys } from 'vs/workbench/contrib/comments/common/commentContextKeys'; export class CommentNode extends Disposable { private _domNode: HTMLElement; @@ -104,7 +105,10 @@ export class CommentNode extends Disposable { this._domNode = dom.$('div.review-comment'); this._contextKeyService = contextKeyService.createScoped(this._domNode); - this._commentContextValue = this._contextKeyService.createKey('comment', comment.contextValue); + this._commentContextValue = CommentContextKeys.commentContext.bindTo(this._contextKeyService); + if (this.comment.contextValue) { + this._commentContextValue.set(this.comment.contextValue); + } this._commentMenus = this.commentService.getCommentMenus(this.owner); this._domNode.tabIndex = -1; diff --git a/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts b/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts index 370a60796b9..7c9a90a09b5 100644 --- a/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts +++ b/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts @@ -105,13 +105,13 @@ export class CommentThreadWidget extends this._styleElement = dom.createStyleSheet(this.container); - this._commentThreadContextValue = this._contextKeyService.createKey('commentThread', undefined); + this._commentThreadContextValue = CommentContextKeys.commentThreadContext.bindTo(this._contextKeyService); this._commentThreadContextValue.set(_commentThread.contextValue); - const commentControllerKey = this._contextKeyService.createKey('commentController', undefined); + const commentControllerKey = CommentContextKeys.commentControllerContext.bindTo(this._contextKeyService); const controller = this.commentService.getCommentController(this._owner); - if (controller) { + if (controller?.contextValue) { commentControllerKey.set(controller.contextValue); } diff --git a/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts b/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts index 1bd0dd8c8d8..882b3371628 100644 --- a/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts +++ b/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts @@ -175,7 +175,7 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget public reveal(commentUniqueId?: number, focus: boolean = false) { if (!this._isExpanded) { - this.show({ lineNumber: this._commentThread.range.endLineNumber, column: 1 }, 2); + this.show(this.arrowPosition(this._commentThread.range), 2); } if (commentUniqueId !== undefined) { @@ -242,6 +242,11 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget this._disposables.add(this._commentThreadWidget); } + private arrowPosition(range: IRange): IPosition { + // Arrow on top edge of zone widget will be at the start of the line if range is multi-line, else at midpoint of range (rounding rightwards) + return { lineNumber: range.endLineNumber, column: range.endLineNumber === range.startLineNumber ? (range.startColumn + range.endColumn + 1) / 2 : 1 }; + } + private deleteCommentThread(): void { this.dispose(); this.commentService.disposeCommentThread(this.owner, this._commentThread.threadId); @@ -260,9 +265,8 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget public expand(): Promise { this._commentThread.collapsibleState = languages.CommentThreadCollapsibleState.Expanded; - const lineNumber = this._commentThread.range.endLineNumber; - this.show({ lineNumber, column: 1 }, 2); + this.show(this.arrowPosition(this._commentThread.range), 2); return Promise.resolve(); } @@ -273,7 +277,7 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget return 0; } - toggleExpand(lineNumber: number) { + toggleExpand() { if (this._isExpanded) { this._commentThread.collapsibleState = languages.CommentThreadCollapsibleState.Collapsed; this.hide(); @@ -282,7 +286,6 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget } } else { this._commentThread.collapsibleState = languages.CommentThreadCollapsibleState.Expanded; - this.show({ lineNumber: lineNumber, column: 1 }, 2); } } @@ -300,6 +303,7 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget const lineNumber = this._commentThread.range.endLineNumber; let shouldMoveWidget = false; if (this._commentGlyph) { + this._commentGlyph.setThreadState(commentThread.state); if (this._commentGlyph.getPosition().position!.lineNumber !== lineNumber) { shouldMoveWidget = true; this._commentGlyph.setLineNumber(lineNumber); @@ -307,11 +311,11 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget } if (shouldMoveWidget && this._isExpanded) { - this.show({ lineNumber, column: 1 }, 2); + this.show(this.arrowPosition(this._commentThread.range), 2); } if (this._commentThread.collapsibleState === languages.CommentThreadCollapsibleState.Expanded) { - this.show({ lineNumber, column: 1 }, 2); + this.show(this.arrowPosition(this._commentThread.range), 2); } else { this.hide(); } @@ -325,15 +329,16 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget this._commentThreadWidget.layout(widthInPixel); } - display(lineNumber: number) { - this._commentGlyph = new CommentGlyphWidget(this.editor, lineNumber); + display(range: IRange) { + this._commentGlyph = new CommentGlyphWidget(this.editor, range.endLineNumber); + this._commentGlyph.setThreadState(this._commentThread.state); this._commentThreadWidget.display(this.editor.getOption(EditorOption.lineHeight)); this._disposables.add(this._commentThreadWidget.onDidResize(dimension => { this._refresh(dimension); })); if (this._commentThread.collapsibleState === languages.CommentThreadCollapsibleState.Expanded) { - this.show({ lineNumber: lineNumber, column: 1 }, 2); + this.show(this.arrowPosition(range), 2); } // If this is a new comment thread awaiting user input then we need to reveal it. @@ -361,15 +366,13 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget } if (shouldMoveWidget && this._isExpanded) { - this.show({ lineNumber, column: 1 }, 2); + this.show(this.arrowPosition(this._commentThread.range), 2); } })); this._commentThreadDisposables.push(this._commentThread.onDidChangeCollapsibleState(state => { if (state === languages.CommentThreadCollapsibleState.Expanded && !this._isExpanded) { - const lineNumber = this._commentThread.range.startLineNumber; - - this.show({ lineNumber, column: 1 }, 2); + this.show(this.arrowPosition(this._commentThread.range), 2); return; } diff --git a/src/vs/workbench/contrib/comments/browser/commentsController.ts b/src/vs/workbench/contrib/comments/browser/commentsController.ts index 60ffd13cac1..3396c53ee4d 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsController.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsController.ts @@ -709,7 +709,7 @@ export class CommentController implements IEditorContribution { return; } const zoneWidget = this.instantiationService.createInstance(ReviewZoneWidget, this.editor, owner, thread, pendingComment); - zoneWidget.display(thread.range.endLineNumber); + zoneWidget.display(thread.range); this._commentWidgets.push(zoneWidget); this.openCommentsView(thread); } @@ -762,7 +762,7 @@ export class CommentController implements IEditorContribution { // The widget's position is undefined until the widget has been displayed, so rely on the glyph position instead const existingCommentsAtLine = this._commentWidgets.filter(widget => widget.getGlyphPosition() === commentRange.endLineNumber); if (existingCommentsAtLine.length) { - existingCommentsAtLine.forEach(widget => widget.toggleExpand(commentRange.endLineNumber)); + existingCommentsAtLine.forEach(widget => widget.toggleExpand()); this.processNextThreadToAdd(); return; } else { diff --git a/src/vs/workbench/contrib/comments/browser/media/review.css b/src/vs/workbench/contrib/comments/browser/media/review.css index 6338595b911..78f8639cf0a 100644 --- a/src/vs/workbench/contrib/comments/browser/media/review.css +++ b/src/vs/workbench/contrib/comments/browser/media/review.css @@ -43,7 +43,7 @@ } .review-widget .body .review-comment .comment-actions .monaco-toolbar { - height: 21px; + height: 22px; } .review-widget .body .review-comment .comment-title .comment-header-info { @@ -467,7 +467,8 @@ div.preview.inline .monaco-editor .comment-range-glyph { background: var(--vscode-editorGutter-commentRangeForeground); } -.monaco-editor .comment-thread:before { +.monaco-editor .comment-thread:before, +.monaco-editor .comment-thread-unresolved:before { background: var(--vscode-editorGutter-commentRangeForeground); } @@ -489,13 +490,15 @@ div.preview.inline .monaco-editor .comment-range-glyph { } .monaco-editor .margin-view-overlays .comment-range-glyph.line-hover, -.monaco-editor .margin-view-overlays .comment-range-glyph.comment-thread { +.monaco-editor .margin-view-overlays .comment-range-glyph.comment-thread, +.monaco-editor .margin-view-overlays .comment-range-glyph.comment-thread-unresolved { margin-left: 13px; } .monaco-editor .margin-view-overlays > div:hover > .comment-range-glyph.comment-diff-added:before, .monaco-editor .margin-view-overlays .comment-range-glyph.line-hover:before, -.monaco-editor .comment-range-glyph.comment-thread:before { +.monaco-editor .comment-range-glyph.comment-thread:before, +.monaco-editor .comment-range-glyph.comment-thread-unresolved:before { position: absolute; height: 100%; width: 9px; @@ -526,12 +529,13 @@ div.preview.inline .monaco-editor .comment-range-glyph { padding-left: 1px; } -.monaco-editor .comment-range-glyph.comment-thread { +.monaco-editor .comment-range-glyph.comment-thread, +.monaco-editor .comment-range-glyph.comment-thread-unresolved { z-index: 20; } -.monaco-editor .comment-range-glyph.comment-thread:before { - content: "\ea6b"; +.monaco-editor .comment-range-glyph.comment-thread:before, +.monaco-editor .comment-range-glyph.comment-thread-unresolved:before { font-family: "codicon"; font-size: 13px; width: 18px !important; @@ -543,6 +547,14 @@ div.preview.inline .monaco-editor .comment-range-glyph { padding-left: 1px; } +.monaco-editor .comment-range-glyph.comment-thread:before { + content: "\ea6b"; + +} +.monaco-editor .comment-range-glyph.comment-thread-unresolved:before { + content: "\ec0a"; +} + .monaco-editor.inline-comment .margin-view-overlays .codicon-folding-expanded, .monaco-editor.inline-comment .margin-view-overlays .codicon-folding-collapsed { margin-left: 11px; diff --git a/src/vs/workbench/contrib/comments/common/commentContextKeys.ts b/src/vs/workbench/contrib/comments/common/commentContextKeys.ts index 3593481d5ca..c4b32556b31 100644 --- a/src/vs/workbench/contrib/comments/common/commentContextKeys.ts +++ b/src/vs/workbench/contrib/comments/common/commentContextKeys.ts @@ -3,15 +3,29 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as nls from 'vs/nls'; import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; + export namespace CommentContextKeys { /** * A context key that is set when the comment thread has no comments. */ - export const commentThreadIsEmpty = new RawContextKey('commentThreadIsEmpty', false); + export const commentThreadIsEmpty = new RawContextKey('commentThreadIsEmpty', false, { type: 'boolean', description: nls.localize('commentThreadIsEmpty', "Set when the comment thread has no comments") }); /** * A context key that is set when the comment has no input. */ - export const commentIsEmpty = new RawContextKey('commentIsEmpty', false); -} \ No newline at end of file + export const commentIsEmpty = new RawContextKey('commentIsEmpty', false, { type: 'boolean', description: nls.localize('commentIsEmpty', "Set when the comment has no input") }); + /** + * The context value of the comment. + */ + export const commentContext = new RawContextKey('comment', undefined, { type: 'string', description: nls.localize('comment', "The context value of the comment") }); + /** + * The context value of the comment thread. + */ + export const commentThreadContext = new RawContextKey('commentThread', undefined, { type: 'string', description: nls.localize('commentThread', "The context value of the comment thread") }); + /** + * The comment controller id associated with a comment thread. + */ + export const commentControllerContext = new RawContextKey('commentController', undefined, { type: 'string', description: nls.localize('commentController', "The comment controller id associated with a comment thread") }); +} diff --git a/src/vs/workbench/contrib/debug/browser/debugActionViewItems.ts b/src/vs/workbench/contrib/debug/browser/debugActionViewItems.ts index b5e9bff1a64..8eae93927bf 100644 --- a/src/vs/workbench/contrib/debug/browser/debugActionViewItems.ts +++ b/src/vs/workbench/contrib/debug/browser/debugActionViewItems.ts @@ -253,7 +253,7 @@ export class StartDebugActionViewItem extends BaseActionViewItem { } } -export class FocusSessionActionViewItem extends SelectActionViewItem { +export class FocusSessionActionViewItem extends SelectActionViewItem { constructor( action: IAction, session: IDebugSession | undefined, @@ -286,7 +286,7 @@ export class FocusSessionActionViewItem extends SelectActionViewItem { this.update(selectedSession); } - protected override getActionContext(_: string, index: number): any { + protected override getActionContext(_: string, index: number): IDebugSession { return this.getSessions()[index]; } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts index df1301577f6..a329755f732 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts @@ -67,7 +67,7 @@ import { flatten } from 'vs/base/common/arrays'; import { fromNow } from 'vs/base/common/date'; import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; import { getLocale } from 'vs/platform/languagePacks/common/languagePacks'; -import { ILocaleService } from 'vs/workbench/contrib/localization/common/locale'; +import { ILocaleService } from 'vs/workbench/services/localization/common/locale'; import { isString } from 'vs/base/common/types'; import { showWindowLogActionId } from 'vs/workbench/common/logConstants'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; @@ -177,7 +177,7 @@ export class PromptExtensionInstallFailureAction extends Action { return undefined; } let targetPlatform = this.extension.gallery.properties.targetPlatform; - if (this.extensionManagementServerService.remoteExtensionManagementServer) { + if (targetPlatform !== TargetPlatform.UNIVERSAL && targetPlatform !== TargetPlatform.UNDEFINED && this.extensionManagementServerService.remoteExtensionManagementServer) { try { const manifest = await this.galleryService.getManifest(this.extension.gallery, CancellationToken.None); if (manifest && this.extensionManifestPropertiesService.prefersExecuteOnWorkspace(manifest)) { diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts index e59656c21ba..c391c705c5f 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts @@ -47,7 +47,7 @@ import { IExtensionService, IExtensionsStatus, toExtension, toExtensionDescripti import { ExtensionEditor } from 'vs/workbench/contrib/extensions/browser/extensionEditor'; import { isWeb, language } from 'vs/base/common/platform'; import { getLocale } from 'vs/platform/languagePacks/common/languagePacks'; -import { ILocaleService } from 'vs/workbench/contrib/localization/common/locale'; +import { ILocaleService } from 'vs/workbench/services/localization/common/locale'; import { TelemetryTrustedValue } from 'vs/platform/telemetry/common/telemetryUtils'; import { ILifecycleService, LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; diff --git a/src/vs/workbench/contrib/files/browser/files.contribution.ts b/src/vs/workbench/contrib/files/browser/files.contribution.ts index 6b216477a06..54f7839e0d2 100644 --- a/src/vs/workbench/contrib/files/browser/files.contribution.ts +++ b/src/vs/workbench/contrib/files/browser/files.contribution.ts @@ -264,7 +264,8 @@ configurationRegistry.registerConfiguration({ type: 'boolean', description: nls.localize('watcherUseUtilityProcess', "When enabled, the file watcher will be launched using the new UtilityProcess Electron API."), default: false, //typeof product.quality === 'string' && product.quality !== 'stable', // disabled by default in stable for now - ignoreSync: true + ignoreSync: true, + 'scope': ConfigurationScope.APPLICATION }, 'files.hotExit': hotExitConfiguration, 'files.defaultLanguage': { diff --git a/src/vs/workbench/contrib/files/browser/media/explorerviewlet.css b/src/vs/workbench/contrib/files/browser/media/explorerviewlet.css index 89e5008d3c4..fb6d440dfaa 100644 --- a/src/vs/workbench/contrib/files/browser/media/explorerviewlet.css +++ b/src/vs/workbench/contrib/files/browser/media/explorerviewlet.css @@ -9,6 +9,15 @@ height: 100%; } +.explorer-item-hover { + /* -- Must set important as hover overrides the cursor -- */ + cursor: pointer !important; + padding-left: 6px; + height: 22px; + font-size: 13px !important; + user-select: none !important; +} + .explorer-folders-view .monaco-list-row { padding-left: 4px; /* align top level twistie with `Explorer` title label */ } diff --git a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts index 65a9ad14614..c515fd7ed6a 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts @@ -48,7 +48,7 @@ import { ITreeCompressionDelegate } from 'vs/base/browser/ui/tree/asyncDataTree' import { ICompressibleTreeRenderer } from 'vs/base/browser/ui/tree/objectTree'; import { ICompressedTreeNode } from 'vs/base/browser/ui/tree/compressedObjectTreeModel'; import { ILabelService } from 'vs/platform/label/common/label'; -import { isNumber } from 'vs/base/common/types'; +import { isNumber, isStringArray } from 'vs/base/common/types'; import { IEditableData } from 'vs/workbench/common/views'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; @@ -62,6 +62,9 @@ import { ResourceSet } from 'vs/base/common/map'; import { TernarySearchTree } from 'vs/base/common/ternarySearchTree'; import { defaultInputBoxStyles } from 'vs/platform/theme/browser/defaultStyles'; import { timeout } from 'vs/base/common/async'; +import { IHoverDelegate, IHoverDelegateOptions, IHoverWidget } from 'vs/base/browser/ui/iconLabel/iconHoverDelegate'; +import { IHoverService } from 'vs/workbench/services/hover/browser/hover'; +import { HoverPosition } from 'vs/base/browser/ui/hover/hoverWidget'; export class ExplorerDelegate implements IListVirtualDelegate { @@ -275,6 +278,77 @@ export class FilesRenderer implements ICompressibleTreeRenderer(); readonly onDidChangeActiveDescendant = this._onDidChangeActiveDescendant.event; + private readonly hoverDelegate = new class implements IHoverDelegate { + + private lastHoverHideTime = 0; + private hiddenFromClick = false; + readonly placement = 'element'; + + get delay() { + // Delay implementation borrowed froms src/vs/workbench/browser/parts/statusbar/statusbarPart.ts + if (Date.now() - this.lastHoverHideTime < 500) { + return 0; // show instantly when a hover was recently shown + } + + return this.configurationService.getValue('workbench.hover.delay'); + } + + constructor( + private readonly configurationService: IConfigurationService, + private readonly hoverService: IHoverService + ) { } + + showHover(options: IHoverDelegateOptions, focus?: boolean): IHoverWidget | undefined { + let element: HTMLElement; + if (options.target instanceof HTMLElement) { + element = options.target; + } else { + element = options.target.targetElements[0]; + } + + const tlRow = element.closest('.monaco-tl-row') as HTMLElement | undefined; + const listRow = tlRow?.closest('.monaco-list-row') as HTMLElement | undefined; + + const child = element.querySelector('div.monaco-icon-label-container') as Element | undefined; + const childOfChild = child?.querySelector('span.monaco-icon-name-container') as HTMLElement | undefined; + let overflowed = false; + if (childOfChild && child) { + const width = child.clientWidth; + const childWidth = childOfChild.offsetWidth; + // Check if element is overflowing its parent container + overflowed = width <= childWidth; + } + + // Only count decorations that provide additional info, as hover overing decorations such as git excluded isn't helpful + const hasDecoration = options.content.toString().includes('•'); + // If it's overflowing or has a decoration show the tooltip + overflowed = overflowed || hasDecoration; + + const indentGuideElement = tlRow?.querySelector('.monaco-tl-indent') as HTMLElement | undefined; + if (!indentGuideElement) { + return; + } + + return overflowed ? this.hoverService.showHover({ + ...options, + target: indentGuideElement, + compact: true, + container: listRow, + additionalClasses: ['explorer-item-hover'], + skipFadeInAnimation: true, + showPointer: false, + hoverPosition: HoverPosition.RIGHT, + }, focus) : undefined; + } + + onDidHideHover(): void { + if (!this.hiddenFromClick) { + this.lastHoverHideTime = Date.now(); + } + this.hiddenFromClick = false; + } + }(this.configurationService, this.hoverService); + constructor( container: HTMLElement, private labels: ResourceLabels, @@ -285,7 +359,8 @@ export class FilesRenderer implements ICompressibleTreeRenderer(); @@ -317,8 +392,7 @@ export class FilesRenderer implements ICompressibleTreeRenderer { try { if (templateData.currentContext) { @@ -417,6 +491,7 @@ export class FilesRenderer implements ICompressibleTreeRenderer action.id === QuickFixAction.ID ? _instantiationService.createInstance(QuickFixActionViewItem, action) : undefined })); - this.icon = dom.append(parent, dom.$('')); + + // wrap the icon in a container that get the icon color as foreground color. That way, if the + // list view does not have a specific color for the icon (=the color variable is invalid) it + // falls back to the foreground color of container (inherit) + this.iconContainer = dom.append(parent, dom.$('')); + this.icon = dom.append(this.iconContainer, dom.$('')); this.messageAndDetailsContainer = dom.append(parent, dom.$('.marker-message-details-container')); } @@ -292,7 +299,8 @@ class MarkerWidget extends Disposable { this.disposables.clear(); dom.clearNode(this.messageAndDetailsContainer); - this.icon.className = `marker-icon codicon ${SeverityIcon.className(MarkerSeverity.toSeverity(element.marker.severity))}`; + this.iconContainer.className = `marker-icon ${Severity.toString(MarkerSeverity.toSeverity(element.marker.severity))}`; + this.icon.className = `codicon ${SeverityIcon.className(MarkerSeverity.toSeverity(element.marker.severity))}`; this.renderQuickfixActionbar(element); this.renderMessageAndDetails(element, filterData); diff --git a/src/vs/workbench/contrib/markers/browser/markersView.ts b/src/vs/workbench/contrib/markers/browser/markersView.ts index e41c8f3b3b8..16f60bdc2e8 100644 --- a/src/vs/workbench/contrib/markers/browser/markersView.ts +++ b/src/vs/workbench/contrib/markers/browser/markersView.ts @@ -478,8 +478,7 @@ export class MarkersView extends FilterViewPane implements IMarkersView { }), expandOnlyOnTwistieClick: (e: MarkerElement) => e instanceof Marker && e.relatedInformation.length > 0, overrideStyles: { - listBackground: this.getBackgroundColor(), - listInactiveSelectionIconForeground: undefined // we don't want to override the severity icon color + listBackground: this.getBackgroundColor() }, selectionNavigation: true, multipleSelectionSupport: true, diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline.ts b/src/vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline.ts index 68769f72685..6e3b8b2c1ad 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline.ts @@ -531,6 +531,10 @@ export class NotebookCellOutline implements IOutline { const markerServiceListener = new MutableDisposable(); this._entriesDisposables.add(markerServiceListener); const updateMarkerUpdater = () => { + if (notebookEditorWidget.isDisposed) { + return; + } + const doUpdateMarker = (clear: boolean) => { for (const entry of this._entries) { if (clear) { diff --git a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts index 238b5e2880c..a461f1e77dc 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts @@ -303,6 +303,10 @@ class CellContentProvider implements ITextModelContentProvider { const ref = await this._notebookModelResolverService.resolve(data.notebook); let result: ITextModel | null = null; + if (!ref.object.isResolved()) { + return null; + } + for (const cell of ref.object.notebook.cells) { if (cell.uri.toString() === resource.toString()) { const bufferFactory: ITextBufferFactory = { @@ -892,7 +896,13 @@ configurationRegistry.registerConfiguration({ type: 'boolean', tags: ['notebookLayout'], default: false - } + }, + [NotebookSetting.outputWordWrap]: { + markdownDescription: nls.localize('notebook.outputWordWrap', "Controls whether the lines in output should wrap."), + type: 'boolean', + tags: ['notebookLayout'], + default: false + }, } }); diff --git a/src/vs/workbench/contrib/notebook/browser/notebookOptions.ts b/src/vs/workbench/contrib/notebook/browser/notebookOptions.ts index 3df4dcbe2b7..e9d1ef61314 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookOptions.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookOptions.ts @@ -75,6 +75,7 @@ export interface NotebookLayoutConfiguration { focusIndicatorGap: number; interactiveWindowCollapseCodeCells: InteractiveWindowCollapseCodeCells; outputScrolling: boolean; + outputWordWrap: boolean; outputLineLimit: number; } @@ -154,6 +155,7 @@ export class NotebookOptions extends Disposable { const interactiveWindowCollapseCodeCells: InteractiveWindowCollapseCodeCells = this.configurationService.getValue(NotebookSetting.interactiveWindowCollapseCodeCells); const outputLineHeight = this._computeOutputLineHeight(); const outputScrolling = this.configurationService.getValue(NotebookSetting.outputScrolling); + const outputWordWrap = this.configurationService.getValue(NotebookSetting.outputWordWrap); const outputLineLimit = this.configurationService.getValue(NotebookSetting.textOutputLineLimit) ?? 30; this._layoutConfiguration = { @@ -193,6 +195,7 @@ export class NotebookOptions extends Disposable { interactiveWindowCollapseCodeCells, markdownFoldHintHeight: 22, outputScrolling: outputScrolling, + outputWordWrap: outputWordWrap, outputLineLimit: outputLineLimit }; @@ -582,6 +585,7 @@ export class NotebookOptions extends Disposable { markupFontSize: this._layoutConfiguration.markupFontSize, outputLineHeight: this._layoutConfiguration.outputLineHeight, outputScrolling: this._layoutConfiguration.outputScrolling, + outputWordWrap: this._layoutConfiguration.outputWordWrap, outputLineLimit: this._layoutConfiguration.outputLineLimit, }; } @@ -602,6 +606,7 @@ export class NotebookOptions extends Disposable { markupFontSize: this._layoutConfiguration.markupFontSize, outputLineHeight: this._layoutConfiguration.outputLineHeight, outputScrolling: this._layoutConfiguration.outputScrolling, + outputWordWrap: this._layoutConfiguration.outputWordWrap, outputLineLimit: this._layoutConfiguration.outputLineLimit, }; } diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts index bfbb1132eaf..17e72fb1bc3 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts @@ -107,6 +107,7 @@ interface BacklayerWebviewOptions { readonly markupFontSize: number; readonly outputLineHeight: number; readonly outputScrolling: boolean; + readonly outputWordWrap: boolean; readonly outputLineLimit: number; } @@ -264,7 +265,8 @@ export class BackLayerWebView extends Themable { const preloadsData = this.getStaticPreloadsData(); const renderOptions = { lineLimit: this.options.outputLineLimit, - outputScrolling: this.options.outputScrolling + outputScrolling: this.options.outputScrolling, + outputWordWrap: this.options.outputWordWrap }; const preloadScript = preloadsScriptStr( this.options, @@ -675,7 +677,9 @@ export class BackLayerWebView extends Themable { 'github-issues.authNow', 'workbench.extensions.search', 'workbench.action.openSettings', - 'notebook.selectKernel', + '_notebook.selectKernel', + // TODO@rebornix explore open output channel with name command + 'jupyter.viewOutput' ], }); return; diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts index 485b6f262cc..50dd53e28aa 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts @@ -67,6 +67,7 @@ export interface PreloadOptions { export interface RenderOptions { readonly lineLimit: number; readonly outputScrolling: boolean; + readonly outputWordWrap: boolean; } interface PreloadContext { @@ -89,6 +90,7 @@ async function webviewPreloads(ctx: PreloadContext) { let isWorkspaceTrusted = ctx.isWorkspaceTrusted; const lineLimit = ctx.renderOptions.lineLimit; const outputScrolling = ctx.renderOptions.outputScrolling; + const outputWordWrap = ctx.renderOptions.outputWordWrap; const acquireVsCodeApi = globalThis.acquireVsCodeApi; const vscode = acquireVsCodeApi(); @@ -1353,6 +1355,7 @@ async function webviewPreloads(ctx: PreloadContext) { settings: { get lineLimit() { return lineLimit; }, get outputScrolling() { return outputScrolling; }, + get outputWordWrap() { return outputWordWrap; }, } }; diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts index 284753928a7..3ca7b327dbc 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts @@ -256,7 +256,7 @@ export abstract class BaseCellViewModel extends Disposable { writeTransientState(editor.getModel(), this._editorTransientState, this._codeEditorService); } - this._textEditor.changeDecorations((accessor) => { + this._textEditor?.changeDecorations((accessor) => { this._resolvedDecorations.forEach((value, key) => { if (key.startsWith('_lazy_')) { // lazy ones diff --git a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts index 3c349b700fc..1cc134ca882 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts @@ -929,6 +929,7 @@ export const NotebookSetting = { outputFontFamily: 'notebook.outputFontFamily', kernelPickerType: 'notebook.kernelPicker.type', outputScrolling: 'notebook.experimental.outputScrolling', + outputWordWrap: 'notebook.output.wordWrap', logging: 'notebook.logging', } as const; diff --git a/src/vs/workbench/contrib/relauncher/browser/relauncher.contribution.ts b/src/vs/workbench/contrib/relauncher/browser/relauncher.contribution.ts index a62d0b3c7b7..45c9bd63d79 100644 --- a/src/vs/workbench/contrib/relauncher/browser/relauncher.contribution.ts +++ b/src/vs/workbench/contrib/relauncher/browser/relauncher.contribution.ts @@ -27,7 +27,7 @@ interface IConfiguration extends IWindowsConfiguration { debug?: { console?: { wordWrap?: boolean } }; editor?: { accessibilitySupport?: 'on' | 'off' | 'auto' }; security?: { workspace?: { trust?: { enabled?: boolean } } }; - window: IWindowSettings & { experimental?: { windowControlsOverlay?: { enabled?: boolean }; useSandbox?: boolean } }; + window: IWindowSettings & { experimental?: { windowControlsOverlay?: { enabled?: boolean }; useSandbox?: boolean; sharedProcessUseUtilityProcess?: boolean } }; workbench?: { enableExperiments?: boolean }; _extensionsGallery?: { enablePPE?: boolean }; } @@ -38,6 +38,7 @@ export class SettingsChangeRelauncher extends Disposable implements IWorkbenchCo 'window.titleBarStyle', 'window.experimental.windowControlsOverlay.enabled', 'window.experimental.useSandbox', + 'window.experimental.sharedProcessUseUtilityProcess', 'files.experimental.watcherUseUtilityProcess', 'window.nativeTabs', 'window.nativeFullScreen', @@ -53,6 +54,7 @@ export class SettingsChangeRelauncher extends Disposable implements IWorkbenchCo private readonly windowControlsOverlayEnabled = new ChangeObserver('boolean'); private readonly windowSandboxEnabled = new ChangeObserver('boolean'); private readonly fileWatcherUtilityProcessEnabled = new ChangeObserver('boolean'); + private readonly sharedProcessUtilityProcessEnabled = new ChangeObserver('boolean'); private readonly nativeTabs = new ChangeObserver('boolean'); private readonly nativeFullScreen = new ChangeObserver('boolean'); private readonly clickThroughInactive = new ChangeObserver('boolean'); @@ -101,6 +103,9 @@ export class SettingsChangeRelauncher extends Disposable implements IWorkbenchCo // File Watcher: Utility Process processChanged(this.fileWatcherUtilityProcessEnabled.handleChange(config.files?.experimental?.watcherUseUtilityProcess)); + // Shared Process: Utility Process + processChanged(this.sharedProcessUtilityProcessEnabled.handleChange(config.window?.experimental?.sharedProcessUseUtilityProcess)); + // macOS: Native tabs processChanged(isMacintosh && this.nativeTabs.handleChange(config.window?.nativeTabs)); diff --git a/src/vs/workbench/contrib/remote/browser/explorerViewItems.ts b/src/vs/workbench/contrib/remote/browser/explorerViewItems.ts index f64ec7f1aab..a1da7375df7 100644 --- a/src/vs/workbench/contrib/remote/browser/explorerViewItems.ts +++ b/src/vs/workbench/contrib/remote/browser/explorerViewItems.ts @@ -26,7 +26,7 @@ interface IRemoteSelectItem extends ISelectOptionItem { virtualWorkspace?: string; } -export class SwitchRemoteViewItem extends SelectActionViewItem { +export class SwitchRemoteViewItem extends SelectActionViewItem { constructor( action: IAction, @@ -92,7 +92,7 @@ export class SwitchRemoteViewItem extends SelectActionViewItem { } } - protected override getActionContext(_: string, index: number): any { + protected override getActionContext(_: string, index: number): IRemoteSelectItem { return this.optionsItems[index]; } diff --git a/src/vs/workbench/contrib/scm/browser/dirtyDiffSwitcher.ts b/src/vs/workbench/contrib/scm/browser/dirtyDiffSwitcher.ts new file mode 100644 index 00000000000..b2db48e1881 --- /dev/null +++ b/src/vs/workbench/contrib/scm/browser/dirtyDiffSwitcher.ts @@ -0,0 +1,64 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as nls from 'vs/nls'; +import { Action, IAction } from 'vs/base/common/actions'; +import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; +import { ISelectOptionItem } from 'vs/base/browser/ui/selectBox/selectBox'; +import { SelectActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; +import { defaultSelectBoxStyles } from 'vs/platform/theme/browser/defaultStyles'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { peekViewTitleBackground } from 'vs/editor/contrib/peekView/browser/peekView'; +import { Color } from 'vs/base/common/color'; + +export interface IQuickDiffSelectItem extends ISelectOptionItem { + provider: string; +} + +export class SwitchQuickDiffViewItem extends SelectActionViewItem { + private readonly optionsItems: IQuickDiffSelectItem[]; + + constructor( + action: IAction, + providers: string[], + selected: string, + @IContextViewService contextViewService: IContextViewService, + @IThemeService themeService: IThemeService + ) { + const items = providers.map(provider => ({ provider, text: provider })); + let startingSelection = providers.indexOf(selected); + if (startingSelection === -1) { + startingSelection = 0; + } + const styles = { ...defaultSelectBoxStyles }; + const theme = themeService.getColorTheme(); + styles.selectBackground = theme.getColor(peekViewTitleBackground)?.lighten(.7).toString() || Color.transparent.toString(); + super(null, action, items, startingSelection, contextViewService, styles, { ariaLabel: nls.localize('remotes', 'Switch quick diff base') }); + this.optionsItems = items; + } + + public setSelection(provider: string) { + const index = this.optionsItems.findIndex(item => item.provider === provider); + this.select(index); + } + + protected override getActionContext(_: string, index: number): IQuickDiffSelectItem { + return this.optionsItems[index]; + } +} + +export class SwitchQuickDiffBaseAction extends Action { + + public static readonly ID = 'quickDiff.base.switch'; + public static readonly LABEL = nls.localize('quickDiff.base.switch', "Switch Quick Diff Base"); + + constructor(private readonly callback: (event?: IQuickDiffSelectItem) => void) { + super(SwitchQuickDiffBaseAction.ID, SwitchQuickDiffBaseAction.LABEL, undefined, undefined); + } + + override async run(event?: IQuickDiffSelectItem): Promise { + return this.callback(event); + } +} diff --git a/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts b/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts index 60cc0583abc..212a40fe506 100644 --- a/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts +++ b/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts @@ -42,7 +42,7 @@ import { OverviewRulerLane, ITextModel, IModelDecorationOptions, MinimapPosition import { equals, sortedDiff } from 'vs/base/common/arrays'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { ISplice } from 'vs/base/common/sequence'; -import { createStyleSheet } from 'vs/base/browser/dom'; +import * as dom from 'vs/base/browser/dom'; import { EncodingMode, ITextFileEditorModel, IResolvedTextFileEditorModel, ITextFileService, isTextFileEditorModel } from 'vs/workbench/services/textfile/common/textfiles'; import { gotoNextLocation, gotoPreviousLocation } from 'vs/platform/theme/common/iconRegistry'; import { Codicon } from 'vs/base/common/codicons'; @@ -59,6 +59,7 @@ import { FILE_EDITOR_INPUT_ID } from 'vs/workbench/contrib/files/common/files'; import { AudioCue, IAudioCueService } from 'vs/platform/audioCues/browser/audioCueService'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; import { IQuickDiffService, QuickDiff } from 'vs/workbench/contrib/scm/common/quickDiff'; +import { IQuickDiffSelectItem, SwitchQuickDiffBaseAction, SwitchQuickDiffViewItem } from 'vs/workbench/contrib/scm/browser/dirtyDiffSwitcher'; class DiffActionRunner extends ActionRunner { @@ -180,9 +181,12 @@ class DirtyDiffWidget extends PeekViewWidget { private diffEditor!: EmbeddedDiffEditorWidget; private title: string; private menu: IMenu; - private index: number = 0; + private _index: number = 0; + private _provider: string = ''; private change: IChange | undefined; private height: number | undefined = undefined; + private dropdown: SwitchQuickDiffViewItem | undefined; + private dropdownContainer: HTMLElement | undefined; constructor( editor: ICodeEditor, @@ -192,7 +196,7 @@ class DirtyDiffWidget extends PeekViewWidget { @IMenuService menuService: IMenuService, @IContextKeyService contextKeyService: IContextKeyService ) { - super(editor, { isResizeable: true, frameWidth: 1, keepEditorSelection: true }, instantiationService); + super(editor, { isResizeable: true, frameWidth: 1, keepEditorSelection: true, className: 'dirty-diff' }, instantiationService); this._disposables.add(themeService.onDidColorThemeChange(this._applyTheme, this)); this._applyTheme(themeService.getColorTheme()); @@ -213,10 +217,24 @@ class DirtyDiffWidget extends PeekViewWidget { this.setTitle(this.title); } - showChange(index: number): void { + get provider(): string { + return this._provider; + } + + get index(): number { + return this._index; + } + + get visibleRange(): Range | undefined { + const visibleRanges = this.diffEditor.getModifiedEditor().getVisibleRanges(); + return visibleRanges.length >= 0 ? visibleRanges[0] : undefined; + } + + showChange(index: number, usePosition: boolean = true): void { const labeledChange = this.model.changes[index]; const change = labeledChange.change; - this.index = index; + this._index = index; + this._provider = labeledChange.label; this.change = change; const originalModel = this.model.original; @@ -236,6 +254,7 @@ class DirtyDiffWidget extends PeekViewWidget { return; } this.diffEditor.setModel(diffEditorModel); + this.dropdown?.setSelection(labeledChange.label); const position = new Position(getModifiedEndLineNumber(change), 1); @@ -244,28 +263,91 @@ class DirtyDiffWidget extends PeekViewWidget { const editorHeightInLines = Math.floor(editorHeight / lineHeight); const height = Math.min(getChangeHeight(change) + /* padding */ 8, Math.floor(editorHeightInLines / 3)); - this.renderTitle(labeledChange.labels); + this.renderTitle(labeledChange.label); const changeType = getChangeType(change); const changeTypeColor = getChangeTypeColor(this.themeService.getColorTheme(), changeType); this.style({ frameColor: changeTypeColor, arrowColor: changeTypeColor }); - this._actionbarWidget!.context = [diffEditorModel.modified.uri, this.model.changes.map(change => change.change), index]; - this.show(position, height); + const providerSpecificChanges: IChange[] = []; + for (const change of this.model.changes) { + if (change.label === this.model.changes[this._index].label) { + providerSpecificChanges.push(change.change); + } + } + this._actionbarWidget!.context = [diffEditorModel.modified.uri, providerSpecificChanges, index]; + if (usePosition) { + this.show(position, height); + } this.editor.focus(); } - private renderTitle(labels: string[]): void { - const detail = this.model.changes.length > 1 - ? nls.localize('changes', "{0} - {1} of {2} changes", labels.join(', '), this.index + 1, this.model.changes.length) - : nls.localize('change', "{0} - {1} of {2} change", labels.join(', '), this.index + 1, this.model.changes.length); + private renderTitle(label: string): void { + const providerChanges = this.model.mapChanges.get(label)!; + const providerIndex = providerChanges.indexOf(this._index); + + let detail: string; + if (!this.shouldUseDropdown()) { + detail = this.model.changes.length > 1 + ? nls.localize('changes', "{0} - {1} of {2} changes", label, providerIndex + 1, providerChanges.length) + : nls.localize('change', "{0} - {1} of {2} change", label, providerIndex + 1, providerChanges.length); + this.dropdownContainer!.style.display = 'none'; + } else { + detail = this.model.changes.length > 1 + ? nls.localize('multiChanges', "{0} of {1} changes", providerIndex + 1, providerChanges.length) + : nls.localize('multiChange', "{0} of {1} change", providerIndex + 1, providerChanges.length); + this.dropdownContainer!.style.display = 'inherit'; + } this.setTitle(this.title, detail); } + private switchQuickDiff(event?: IQuickDiffSelectItem) { + const newProvider = event?.provider; + if (newProvider === this.model.changes[this._index].label) { + return; + } + let closestGreaterIndex = this._index < this.model.changes.length - 1 ? this._index + 1 : 0; + for (let i = closestGreaterIndex; i !== this._index; i < this.model.changes.length - 1 ? i++ : i = 0) { + if (this.model.changes[i].label === newProvider) { + closestGreaterIndex = i; + break; + } + } + let closestLesserIndex = this._index > 0 ? this._index - 1 : this.model.changes.length - 1; + for (let i = closestLesserIndex; i !== this._index; i >= 0 ? i-- : i = this.model.changes.length - 1) { + if (this.model.changes[i].label === newProvider) { + closestLesserIndex = i; + break; + } + } + const closestIndex = Math.abs(this.model.changes[closestGreaterIndex].change.modifiedEndLineNumber - this.model.changes[this._index].change.modifiedEndLineNumber) + < Math.abs(this.model.changes[closestLesserIndex].change.modifiedEndLineNumber - this.model.changes[this._index].change.modifiedEndLineNumber) + ? closestGreaterIndex : closestLesserIndex; + this.showChange(closestIndex, false); + } + + private shouldUseDropdown(): boolean { + let providersWithChangesCount = 0; + if (this.model.mapChanges.size > 1) { + const keys = Array.from(this.model.mapChanges.keys()); + for (let i = 0; (i < keys.length) && (providersWithChangesCount <= 1); i++) { + if (this.model.mapChanges.get(keys[i])!.length > 0) { + providersWithChangesCount++; + } + } + } + return providersWithChangesCount >= 2; + } + protected override _fillHead(container: HTMLElement): void { super._fillHead(container, true); + this.dropdownContainer = dom.prepend(this._titleElement!, dom.$('.dropdown')); + this.dropdown = this.instantiationService.createInstance(SwitchQuickDiffViewItem, new SwitchQuickDiffBaseAction((event?: IQuickDiffSelectItem) => this.switchQuickDiff(event)), + this.model.quickDiffs.map(quickDiffer => quickDiffer.label), this.model.changes[this._index].label); + this.dropdown.render(this.dropdownContainer); + const previous = this.instantiationService.createInstance(UIEditorAction, this.editor, new ShowPreviousChangeAction(this.editor), ThemeIcon.asClassName(gotoPreviousLocation)); const next = this.instantiationService.createInstance(UIEditorAction, this.editor, new ShowNextChangeAction(this.editor), ThemeIcon.asClassName(gotoNextLocation)); @@ -606,7 +688,6 @@ export class DirtyDiffController extends Disposable implements DirtyDiffContribu private model: DirtyDiffModel | null = null; private widget: DirtyDiffWidget | null = null; - private currentIndex: number = -1; private readonly isDirtyDiffVisible!: IContextKey; private session: IDisposable = Disposable.None; private mouseDownInfo: { lineNumber: number } | null = null; @@ -622,7 +703,7 @@ export class DirtyDiffController extends Disposable implements DirtyDiffContribu ) { super(); this.enabled = !contextKeyService.getContextKeyValue('isInDiffEditor'); - this.stylesheet = createStyleSheet(); + this.stylesheet = dom.createStyleSheet(); this._register(toDisposable(() => this.stylesheet.remove())); if (this.enabled) { @@ -667,7 +748,11 @@ export class DirtyDiffController extends Disposable implements DirtyDiffContribu } canNavigate(): boolean { - return this.currentIndex === -1 || (!!this.model && this.model.changes.length > 1); + return this.widget?.index === -1 || (!!this.model && this.model.changes.length > 1); + } + + refresh(): void { + this.widget?.showChange(this.widget.index, false); } next(lineNumber?: number): void { @@ -678,13 +763,16 @@ export class DirtyDiffController extends Disposable implements DirtyDiffContribu return; } - if (this.editor.hasModel() && (typeof lineNumber === 'number' || this.currentIndex === -1)) { - this.currentIndex = this.model.findNextClosestChange(typeof lineNumber === 'number' ? lineNumber : this.editor.getPosition().lineNumber); + let index: number; + if (this.editor.hasModel() && (typeof lineNumber === 'number')) { + index = this.model.findNextClosestChange(typeof lineNumber === 'number' ? lineNumber : this.editor.getPosition().lineNumber, true, this.widget.provider); } else { - this.currentIndex = rot(this.currentIndex + 1, this.model.changes.length); + const providerChanges: number[] = this.model.mapChanges.get(this.widget.provider) ?? this.model.mapChanges.values().next().value; + const mapIndex = providerChanges.findIndex(value => value === this.widget!.index); + index = providerChanges[rot(mapIndex + 1, providerChanges.length)]; } - this.widget.showChange(this.currentIndex); + this.widget.showChange(index); } previous(lineNumber?: number): void { @@ -695,13 +783,16 @@ export class DirtyDiffController extends Disposable implements DirtyDiffContribu return; } - if (this.editor.hasModel() && (typeof lineNumber === 'number' || this.currentIndex === -1)) { - this.currentIndex = this.model.findPreviousClosestChange(typeof lineNumber === 'number' ? lineNumber : this.editor.getPosition().lineNumber); + let index: number; + if (this.editor.hasModel() && (typeof lineNumber === 'number')) { + index = this.model.findPreviousClosestChange(typeof lineNumber === 'number' ? lineNumber : this.editor.getPosition().lineNumber, true, this.widget.provider); } else { - this.currentIndex = rot(this.currentIndex - 1, this.model.changes.length); + const providerChanges: number[] = this.model.mapChanges.get(this.widget.provider) ?? this.model.mapChanges.values().next().value; + const mapIndex = providerChanges.findIndex(value => value === this.widget!.index); + index = providerChanges[rot(mapIndex - 1, providerChanges.length)]; } - this.widget.showChange(this.currentIndex); + this.widget.showChange(index); } close(): void { @@ -743,7 +834,6 @@ export class DirtyDiffController extends Disposable implements DirtyDiffContribu return false; } - this.currentIndex = -1; this.model = model; this.widget = this.instantiationService.createInstance(DirtyDiffWidget, this.editor, model); this.isDirtyDiffVisible.set(true); @@ -759,7 +849,6 @@ export class DirtyDiffController extends Disposable implements DirtyDiffContribu disposables.add(toDisposable(() => { this.model = null; this.widget = null; - this.currentIndex = -1; this.isDirtyDiffVisible.set(false); this.editor.focus(); })); @@ -774,16 +863,13 @@ export class DirtyDiffController extends Disposable implements DirtyDiffContribu } for (const splice of splices) { - if (splice.start <= this.currentIndex) { - if (this.currentIndex < splice.start + splice.deleteCount) { - this.currentIndex = -1; - this.next(); - } else { - this.currentIndex = rot(this.currentIndex + splice.toInsert.length - splice.deleteCount - 1, this.model.changes.length); - this.next(); - } + if (splice.start <= this.widget.index) { + this.next(); + return; } } + + this.refresh(); } private onEditorMouseDown(e: IEditorMouseEvent): void { @@ -861,7 +947,7 @@ export class DirtyDiffController extends Disposable implements DirtyDiffContribu return; } - if (index === this.currentIndex) { + if (index === this.widget?.index) { this.close(); } else { this.next(lineNumber); @@ -1110,7 +1196,7 @@ export async function getOriginalResource(quickDiffService: IQuickDiffService, u return quickDiffs.length > 0 ? quickDiffs[0].originalResource : null; } -type LabeledChange = { change: IChange; labels: string[]; uri: URI }; +type LabeledChange = { change: IChange; label: string; uri: URI }; export class DirtyDiffModel extends Disposable { @@ -1120,7 +1206,7 @@ export class DirtyDiffModel extends Disposable { private _model: ITextFileEditorModel; get original(): ITextModel[] { return this._originalTextModels; } - private diffDelayer = new ThrottledDelayer(200); + private diffDelayer = new ThrottledDelayer<{ changes: LabeledChange[]; mapChanges: Map } | null>(200); private _quickDiffsPromise?: Promise; private repositoryDisposables = new Set(); private readonly originalModelDisposables = this._register(new DisposableStore()); @@ -1131,6 +1217,8 @@ export class DirtyDiffModel extends Disposable { private _changes: LabeledChange[] = []; get changes(): LabeledChange[] { return this._changes; } + private _mapChanges: Map = new Map(); // key is the quick diff name, value is the index of the change in this._changes + get mapChanges(): Map { return this._mapChanges; } constructor( textFileModel: IResolvedTextFileEditorModel, @@ -1161,13 +1249,18 @@ export class DirtyDiffModel extends Disposable { this._originalModels.clear(); this._originalTextModels = []; this._quickDiffsPromise = undefined; - this.setChanges([]); + this.setChanges([], new Map()); this.triggerDiff(); })); + this._register(this.quickDiffService.onDidChangeQuickDiffProviders(() => this.triggerDiff())); this.triggerDiff(); } + get quickDiffs(): readonly QuickDiff[] { + return this._quickDiffs; + } + public getDiffEditorModel(originalUri: string): IDiffEditorModel | undefined { if (!this._originalModels.has(originalUri)) { return; @@ -1202,40 +1295,41 @@ export class DirtyDiffModel extends Disposable { return this.diffDelayer .trigger(() => this.diff()) - .then((changes: LabeledChange[] | null) => { + .then((result: { changes: LabeledChange[]; mapChanges: Map } | null) => { const originalModels = Array.from(this._originalModels.values()); - if (this._disposed || this._model.isDisposed() || originalModels.some(originalModel => originalModel.isDisposed())) { + if (!result || this._disposed || this._model.isDisposed() || originalModels.some(originalModel => originalModel.isDisposed())) { return; // disposed } if (originalModels.every(originalModel => originalModel.textEditorModel.getValueLength() === 0)) { - changes = []; + result.changes = []; } - if (!changes) { - changes = []; + if (!result.changes) { + result.changes = []; } - this.setChanges(changes); + this.setChanges(result.changes, result.mapChanges); }, (err) => onUnexpectedError(err)); } - private setChanges(changes: LabeledChange[]): void { + private setChanges(changes: LabeledChange[], mapChanges: Map): void { const diff = sortedDiff(this._changes, changes, (a, b) => compareChanges(a.change, b.change)); this._changes = changes; + this._mapChanges = mapChanges; this._onDidChange.fire({ changes, diff }); } - private diff(): Promise { + private diff(): Promise<{ changes: LabeledChange[]; mapChanges: Map } | null> { return this.progressService.withProgress({ location: ProgressLocation.Scm, delay: 250 }, async () => { const originalURIs = await this.getQuickDiffsPromise(); if (this._disposed || this._model.isDisposed() || (originalURIs.length === 0)) { - return Promise.resolve([]); // disposed + return Promise.resolve({ changes: [], mapChanges: new Map() }); // disposed } const filteredToDiffable = originalURIs.filter(quickDiff => this.editorWorkerService.canComputeDirtyDiff(quickDiff.originalResource, this._model.resource)); if (filteredToDiffable.length === 0) { - return Promise.resolve([]); // All files are too large + return Promise.resolve({ changes: [], mapChanges: new Map() }); // All files are too large } const ignoreTrimWhitespaceSetting = this.configurationService.getValue<'true' | 'false' | 'inherit'>('scm.diffDecorationsIgnoreTrimWhitespace'); @@ -1243,32 +1337,27 @@ export class DirtyDiffModel extends Disposable { ? this.configurationService.getValue('diffEditor.ignoreTrimWhitespace') : ignoreTrimWhitespaceSetting !== 'false'; - const allDiffs: { change: IChange; labels: string[]; uri: URI }[] = []; + const allDiffs: LabeledChange[] = []; for (const quickDiff of filteredToDiffable) { const dirtyDiff = await this.editorWorkerService.computeDirtyDiff(quickDiff.originalResource, this._model.resource, ignoreTrimWhitespace); if (dirtyDiff) { for (const diff of dirtyDiff) { if (diff) { - allDiffs.push({ change: diff, labels: [quickDiff.label], uri: quickDiff.originalResource }); + allDiffs.push({ change: diff, label: quickDiff.label, uri: quickDiff.originalResource }); } } } } const sorted = allDiffs.sort((a, b) => compareChanges(a.change, b.change)); - const reduced = []; - for (const diff of sorted) { - if (reduced.length === 0) { - reduced.push(diff); - } else { - const comparedChanges = compareChanges(reduced[reduced.length - 1].change, diff.change); - if (comparedChanges === 0) { - reduced[reduced.length - 1].labels.push(...diff.labels); - } else { - reduced.push(diff); - } + const map: Map = new Map(); + for (let i = 0; i < sorted.length; i++) { + const label = sorted[i].label; + if (!map.has(label)) { + map.set(label, []); } + map.get(label)!.push(i); } - return reduced; + return { changes: sorted, mapChanges: map }; }); } @@ -1340,8 +1429,11 @@ export class DirtyDiffModel extends Disposable { return this.quickDiffService.getQuickDiffs(uri, this._model.getLanguageId(), this._model.textEditorModel ? shouldSynchronizeModel(this._model.textEditorModel) : undefined); } - findNextClosestChange(lineNumber: number, inclusive = true): number { + findNextClosestChange(lineNumber: number, inclusive = true, provider?: string): number { for (let i = 0; i < this.changes.length; i++) { + if (provider && this.changes[i].label !== provider) { + continue; + } const change = this.changes[i].change; if (inclusive) { @@ -1358,8 +1450,11 @@ export class DirtyDiffModel extends Disposable { return 0; } - findPreviousClosestChange(lineNumber: number, inclusive = true): number { + findPreviousClosestChange(lineNumber: number, inclusive = true, provider?: string): number { for (let i = this.changes.length - 1; i >= 0; i--) { + if (provider && this.changes[i].label !== provider) { + continue; + } const change = this.changes[i].change; if (inclusive) { @@ -1422,7 +1517,7 @@ export class DirtyDiffWorkbenchController extends Disposable implements ext.IWor @ITextFileService private readonly textFileService: ITextFileService ) { super(); - this.stylesheet = createStyleSheet(); + this.stylesheet = dom.createStyleSheet(); this._register(toDisposable(() => this.stylesheet.parentElement!.removeChild(this.stylesheet))); const onDidChangeConfiguration = Event.filter(configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('scm.diffDecorations')); diff --git a/src/vs/workbench/contrib/scm/browser/media/dirtydiffDecorator.css b/src/vs/workbench/contrib/scm/browser/media/dirtydiffDecorator.css index de528c90683..fe5686a3eee 100644 --- a/src/vs/workbench/contrib/scm/browser/media/dirtydiffDecorator.css +++ b/src/vs/workbench/contrib/scm/browser/media/dirtydiffDecorator.css @@ -93,3 +93,18 @@ bottom: 0; transition: height 80ms linear; } + +.dirty-diff .peekview-title .dropdown { + margin-right: 10px; +} + +.dirty-diff .peekview-title .dropdown.select-container { + cursor: default; +} + +.dirty-diff .peekview-title .dropdown .monaco-select-box { + cursor: pointer; + min-width: 100px; + min-height: 18px; + padding: 0px 23px 0px 8px; +} diff --git a/src/vs/workbench/contrib/scm/common/quickDiff.ts b/src/vs/workbench/contrib/scm/common/quickDiff.ts index 083dca3349c..34caf0865dd 100644 --- a/src/vs/workbench/contrib/scm/common/quickDiff.ts +++ b/src/vs/workbench/contrib/scm/common/quickDiff.ts @@ -7,6 +7,7 @@ import { URI } from 'vs/base/common/uri'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IDisposable } from 'vs/base/common/lifecycle'; import { LanguageSelector } from 'vs/editor/common/languageSelector'; +import { Event } from 'vs/base/common/event'; export const IQuickDiffService = createDecorator('quickDiff'); @@ -25,6 +26,7 @@ export interface QuickDiff { export interface IQuickDiffService { readonly _serviceBrand: undefined; + readonly onDidChangeQuickDiffProviders: Event; addQuickDiffProvider(quickDiff: QuickDiffProvider): IDisposable; getQuickDiffs(uri: URI, language?: string, isSynchronized?: boolean): Promise; } diff --git a/src/vs/workbench/contrib/scm/common/quickDiffService.ts b/src/vs/workbench/contrib/scm/common/quickDiffService.ts index bc87e51cc0e..384cbcf1a5d 100644 --- a/src/vs/workbench/contrib/scm/common/quickDiffService.ts +++ b/src/vs/workbench/contrib/scm/common/quickDiffService.ts @@ -4,10 +4,11 @@ *--------------------------------------------------------------------------------------------*/ import { URI } from 'vs/base/common/uri'; -import { IDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { IQuickDiffService, QuickDiff, QuickDiffProvider } from 'vs/workbench/contrib/scm/common/quickDiff'; import { isEqualOrParent } from 'vs/base/common/resources'; import { score } from 'vs/editor/common/languageSelector'; +import { Emitter } from 'vs/base/common/event'; function createProviderComparer(uri: URI): (a: QuickDiffProvider, b: QuickDiffProvider) => number { return (a, b) => { @@ -34,16 +35,20 @@ function createProviderComparer(uri: URI): (a: QuickDiffProvider, b: QuickDiffPr }; } -export class QuickDiffService implements IQuickDiffService { +export class QuickDiffService extends Disposable implements IQuickDiffService { declare readonly _serviceBrand: undefined; private quickDiffProviders: Set = new Set(); + private readonly _onDidChangeQuickDiffProviders = this._register(new Emitter()); + readonly onDidChangeQuickDiffProviders = this._onDidChangeQuickDiffProviders.event; addQuickDiffProvider(quickDiff: QuickDiffProvider): IDisposable { this.quickDiffProviders.add(quickDiff); + this._onDidChangeQuickDiffProviders.fire(); return { dispose: () => { this.quickDiffProviders.delete(quickDiff); + this._onDidChangeQuickDiffProviders.fire(); } }; } diff --git a/src/vs/workbench/contrib/search/browser/search.contribution.ts b/src/vs/workbench/contrib/search/browser/search.contribution.ts index 652eb800920..8f0b863a10a 100644 --- a/src/vs/workbench/contrib/search/browser/search.contribution.ts +++ b/src/vs/workbench/contrib/search/browser/search.contribution.ts @@ -352,7 +352,7 @@ configurationRegistry.registerConfiguration({ }, 'search.experimental.notebookSearch': { type: 'boolean', - description: nls.localize('search.experimental.notebookSearch', "Controls whether to use the experimental notebook search in the global search."), + description: nls.localize('search.experimental.notebookSearch', "Controls whether to use the experimental notebook search in the global search. Please refresh your search for changes to this setting to take effect."), default: false }, } diff --git a/src/vs/workbench/contrib/search/test/electron-browser/textsearch.perf.integrationTest.ts b/src/vs/workbench/contrib/search/test/electron-browser/textsearch.perf.integrationTest.ts index 346fa88d3cb..28324a6ebd2 100644 --- a/src/vs/workbench/contrib/search/test/electron-browser/textsearch.perf.integrationTest.ts +++ b/src/vs/workbench/contrib/search/test/electron-browser/textsearch.perf.integrationTest.ts @@ -27,7 +27,6 @@ import { INotificationService } from 'vs/platform/notification/common/notificati import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService'; import { ClassifiedEvent, IGDPRProperty, OmitMetadata, StrictPropertyCheck } from 'vs/platform/telemetry/common/gdprTypings'; import { ITelemetryInfo, ITelemetryService, TelemetryLevel } from 'vs/platform/telemetry/common/telemetry'; -import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService'; import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; import { UndoRedoService } from 'vs/platform/undoRedo/common/undoRedoService'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; @@ -41,8 +40,6 @@ import { IUntitledTextEditorService, UntitledTextEditorService } from 'vs/workbe import { TestEditorGroupsService, TestEditorService } from 'vs/workbench/test/browser/workbenchTestServices'; import { TestContextService, TestTextResourcePropertiesService } from 'vs/workbench/test/common/workbenchTestServices'; import { TestEnvironmentService } from 'vs/workbench/test/electron-browser/workbenchTestServices'; -import { LanguageFeatureDebounceService } from 'vs/editor/common/services/languageFeatureDebounce'; -import { LanguageFeaturesService } from 'vs/editor/common/services/languageFeaturesService'; // declare var __dirname: string; @@ -85,13 +82,9 @@ suite.skip('TextSearch performance (integration)', () => { new ModelService( configurationService, textResourcePropertiesService, - new TestThemeService(), - logService, undoRedoService, new LanguageService(), new TestLanguageConfigurationService(), - new LanguageFeatureDebounceService(logService), - new LanguageFeaturesService() ), ], [ diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.ts b/src/vs/workbench/contrib/terminal/browser/terminal.ts index 75dee393f73..15d579d37c8 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.ts @@ -1053,7 +1053,7 @@ export interface IXtermTerminal { /** * Focuses the accessible buffer, updating its contents */ - focusAccessibleBuffer(): void; + focusAccessibleBuffer(): Promise; } export interface IInternalXtermTerminal { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts index 847fb588711..31b34991131 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts @@ -423,7 +423,7 @@ export function registerTerminalActions() { }); } async run(accessor: ServicesAccessor): Promise { - accessor.get(ITerminalService).activeInstance?.xterm?.focusAccessibleBuffer(); + await accessor.get(ITerminalService).activeInstance?.xterm?.focusAccessibleBuffer(); } }); registerAction2(class extends Action2 { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index 2fa0b772354..8b1f31a4e41 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -473,7 +473,6 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { } return findWidget; }); - this._logService.trace(`terminalInstance#ctor (instanceId: ${this.instanceId})`, this._shellLaunchConfig); this._register(this.capabilities.onDidAddCapability(e => { this._logService.debug('terminalInstance added capability', e); diff --git a/src/vs/workbench/contrib/terminal/browser/terminalMainContribution.ts b/src/vs/workbench/contrib/terminal/browser/terminalMainContribution.ts index 9e722549503..46a75e59421 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalMainContribution.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalMainContribution.ts @@ -5,8 +5,11 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { TabFocus } from 'vs/editor/browser/config/tabFocus'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { ILabelService } from 'vs/platform/label/common/label'; +import { TerminalSettingId } from 'vs/platform/terminal/common/terminal'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { ITerminalEditorService, ITerminalGroupService, ITerminalService, terminalEditorId } from 'vs/workbench/contrib/terminal/browser/terminal'; import { terminalStrings } from 'vs/workbench/contrib/terminal/common/terminalStrings'; @@ -18,13 +21,16 @@ import { IEditorResolverService, RegisteredEditorPriority } from 'vs/workbench/s * be more relevant). */ export class TerminalMainContribution extends Disposable implements IWorkbenchContribution { + private _editorTabFocus: boolean | undefined; + private _terminalTabFocus: boolean | undefined; constructor( @IEditorResolverService editorResolverService: IEditorResolverService, - @IEnvironmentService environmentService: IEnvironmentService, @ILabelService labelService: ILabelService, @ITerminalService terminalService: ITerminalService, @ITerminalEditorService terminalEditorService: ITerminalEditorService, - @ITerminalGroupService terminalGroupService: ITerminalGroupService + @ITerminalGroupService terminalGroupService: ITerminalGroupService, + @IConfigurationService configurationService: IConfigurationService, + @IContextKeyService contextKeyService: IContextKeyService ) { super(); @@ -70,6 +76,33 @@ export class TerminalMainContribution extends Disposable implements IWorkbenchCo separator: '' } }); - } + const viewKey = new Set(); + viewKey.add('focusedView'); + TabFocus.onDidChangeTabFocus(tabFocus => { + if (contextKeyService.getContextKeyValue('focusedView') === 'terminal') { + this._terminalTabFocus = tabFocus; + } else { + this._editorTabFocus = tabFocus; + } + }); + this._register(contextKeyService.onDidChangeContext((c) => { + if (c.affectsSome(viewKey)) { + if (contextKeyService.getContextKeyValue('focusedView') === 'terminal') { + this._editorTabFocus = TabFocus.getTabFocusMode(); + TabFocus.setTabFocusMode(this._terminalTabFocus ?? configurationService.getValue(TerminalSettingId.TabFocusMode)); + } else { + this._terminalTabFocus = TabFocus.getTabFocusMode(); + TabFocus.setTabFocusMode(this._editorTabFocus ?? configurationService.getValue('editor.tabFocusMode')); + } + } + })); + this._register(configurationService.onDidChangeConfiguration(async e => { + if (e.affectsConfiguration('editor.tabFocusMode')) { + this._editorTabFocus = undefined; + } else if (e.affectsConfiguration(TerminalSettingId.TabFocusMode)) { + this._terminalTabFocus = undefined; + } + })); + } } diff --git a/src/vs/workbench/contrib/terminal/browser/xterm/decorationAddon.ts b/src/vs/workbench/contrib/terminal/browser/xterm/decorationAddon.ts index bb8d82f88ab..4c9e394aa06 100644 --- a/src/vs/workbench/contrib/terminal/browser/xterm/decorationAddon.ts +++ b/src/vs/workbench/contrib/terminal/browser/xterm/decorationAddon.ts @@ -26,6 +26,7 @@ import { ITerminalCommand } from 'vs/workbench/contrib/terminal/common/terminal' import { TERMINAL_COMMAND_DECORATION_DEFAULT_BACKGROUND_COLOR, TERMINAL_COMMAND_DECORATION_ERROR_BACKGROUND_COLOR, TERMINAL_COMMAND_DECORATION_SUCCESS_BACKGROUND_COLOR } from 'vs/workbench/contrib/terminal/common/terminalColorRegistry'; import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IDecoration, ITerminalAddon, Terminal } from 'xterm'; +import { AudioCue, IAudioCueService } from 'vs/platform/audioCues/browser/audioCueService'; interface IDisposableDecoration { decoration: IDecoration; disposables: IDisposable[]; exitCode?: number; markProperties?: IMarkProperties } @@ -51,7 +52,8 @@ export class DecorationAddon extends Disposable implements ITerminalAddon { @IQuickInputService private readonly _quickInputService: IQuickInputService, @ILifecycleService lifecycleService: ILifecycleService, @ICommandService private readonly _commandService: ICommandService, - @IInstantiationService instantiationService: IInstantiationService + @IInstantiationService instantiationService: IInstantiationService, + @IAudioCueService private readonly _audioCueService: IAudioCueService ) { super(); this._register(toDisposable(() => this._dispose())); @@ -217,7 +219,12 @@ export class DecorationAddon extends Disposable implements ITerminalAddon { for (const command of capability.commands) { this.registerCommandDecoration(command); } - commandDetectionListeners.push(capability.onCommandFinished(command => this.registerCommandDecoration(command))); + commandDetectionListeners.push(capability.onCommandFinished(command => { + this.registerCommandDecoration(command); + if (command.exitCode) { + this._audioCueService.playAudioCue(AudioCue.terminalCommandFailed); + } + })); // Command invalidated commandDetectionListeners.push(capability.onCommandInvalidated(commands => { for (const command of commands) { diff --git a/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts b/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts index 04185c84067..d83ddae19c6 100644 --- a/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts @@ -38,8 +38,16 @@ import { Emitter } from 'vs/base/common/event'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { SuggestAddon } from 'vs/workbench/contrib/terminal/browser/xterm/suggestAddon'; import { IContextKey } from 'vs/platform/contextkey/common/contextkey'; -import { isLinux } from 'vs/base/common/platform'; -import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; +import { URI } from 'vs/base/common/uri'; +import { ITextModel } from 'vs/editor/common/model'; +import { IModelService } from 'vs/editor/common/services/model'; +import { StringBuilder } from 'vs/editor/common/core/stringBuilder'; +import { CodeEditorWidget, ICodeEditorWidgetOptions } from 'vs/editor/browser/widget/codeEditorWidget'; +import { EditorExtensionsRegistry } from 'vs/editor/browser/editorExtensions'; +import { getSimpleEditorOptions } from 'vs/workbench/contrib/codeEditor/browser/simpleEditorOptions'; +import { IEditorConstructionOptions } from 'vs/editor/browser/config/editorConfiguration'; +import { LinkDetector } from 'vs/editor/contrib/links/browser/links'; +import { SelectionClipboardContributionID } from 'vs/workbench/contrib/codeEditor/browser/selectionClipboard'; const enum RenderConstants { /** @@ -281,7 +289,7 @@ export class XtermTerminal extends DisposableStore implements IXtermTerminal, II }); } - focusAccessibleBuffer(): void { + async focusAccessibleBuffer(): Promise { this._accessibileBuffer?.focus(); } @@ -312,7 +320,7 @@ export class XtermTerminal extends DisposableStore implements IXtermTerminal, II if (!this._container) { this.raw.open(container); } - this._accessibileBuffer = this._instantiationService.createInstance(AccessibleBuffer, this.raw, this.getFont(), this._capabilities); + this._accessibileBuffer = this._instantiationService.createInstance(AccessibleBuffer, this, this._capabilities); // TODO: Move before open to the DOM renderer doesn't initialize if (this._shouldLoadWebgl()) { this._enableWebglRenderer(); @@ -763,76 +771,126 @@ export class XtermTerminal extends DisposableStore implements IXtermTerminal, II this.raw.write(data); } } - +const enum ACCESSIBLE_BUFFER { Scheme = 'terminal-accessible-buffer' } class AccessibleBuffer extends DisposableStore { - - private _accessibleBuffer: HTMLElement | undefined; - private _bufferElementFragment: DocumentFragment | undefined; + private _accessibleBuffer: HTMLElement; + private _bufferEditor: CodeEditorWidget; + private _editorContainer: HTMLElement; + private _registered: boolean = false; + private _font: ITerminalFont; constructor( - private readonly _terminal: RawXtermTerminal, - private readonly _font: ITerminalFont, + private readonly _terminal: XtermTerminal, private readonly _capabilities: ITerminalCapabilityStore, - @IAccessibilityService private readonly _accessibilityService: IAccessibilityService, - @IConfigurationService private readonly _configurationService: IConfigurationService + @IInstantiationService private readonly _instantiationService: IInstantiationService, + @IModelService private readonly _modelService: IModelService, + @IConfigurationService configurationService: IConfigurationService ) { super(); - this.add(this._terminal.registerBufferElementProvider({ provideBufferElements: () => this.focus() })); + const codeEditorWidgetOptions: ICodeEditorWidgetOptions = { + isSimpleWidget: true, + contributions: EditorExtensionsRegistry.getSomeEditorContributions([LinkDetector.ID, SelectionClipboardContributionID]) + }; + this._font = this._terminal.getFont(); + const editorOptions: IEditorConstructionOptions = { + ...getSimpleEditorOptions(), + lineDecorationsWidth: 6, + dragAndDrop: true, + cursorWidth: 1, + fontSize: this._font.fontSize, + lineHeight: this._font.charHeight ? this._font.charHeight * this._font.lineHeight : 1, + fontFamily: this._font.fontFamily, + wrappingStrategy: 'advanced', + wrappingIndent: 'none', + padding: { top: 2, bottom: 2 }, + quickSuggestions: false, + scrollbar: { alwaysConsumeMouseWheel: false }, + renderWhitespace: 'none', + dropIntoEditor: { enabled: true }, + accessibilitySupport: configurationService.getValue<'auto' | 'off' | 'on'>('editor.accessibilitySupport'), + cursorBlinking: configurationService.getValue('terminal.integrated.cursorBlinking'), + readOnly: true + }; + this._accessibleBuffer = this._terminal.raw.element!.querySelector('.xterm-accessible-buffer') as HTMLElement; + this._editorContainer = document.createElement('div'); + this._bufferEditor = this._instantiationService.createInstance(CodeEditorWidget, this._editorContainer, editorOptions, codeEditorWidgetOptions); + this.add(configurationService.onDidChangeConfiguration(e => { + if (e.affectedKeys.has(TerminalSettingId.FontFamily)) { + this._font = this._terminal.getFont(); + } + })); } - focus(): DocumentFragment { - if (!this._bufferElementFragment) { - this._bufferElementFragment = document.createDocumentFragment(); - } - this._accessibleBuffer = this._terminal.element?.querySelector('.xterm-accessible-buffer') as HTMLElement || undefined; - if (!this._accessibleBuffer) { - return this._bufferElementFragment; - } - // see https://github.com/microsoft/vscode/issues/173532 - const accessibleBufferContentEditable = isLinux ? 'on' : this._configurationService.getValue(TerminalSettingId.AccessibleBufferContentEditable); - this._accessibleBuffer.contentEditable = accessibleBufferContentEditable === 'on' || (accessibleBufferContentEditable === 'auto' && !this._accessibilityService.isScreenReaderOptimized()) ? 'true' : 'false'; - // The viewport is undefined when this is focused, so we cannot get the cell height from that. Instead, estimate using the font. - const lineHeight = this._font?.charHeight ? this._font.charHeight * this._font.lineHeight + 'px' : ''; - this._accessibleBuffer.style.lineHeight = lineHeight; - const commands = this._capabilities.get(TerminalCapability.CommandDetection)?.commands; - if (!commands?.length) { - const noContent = document.createElement('div'); - const noContentLabel = localize('terminal.integrated.noContent', "No terminal content available for this session."); - noContent.textContent = noContentLabel; - this._bufferElementFragment.replaceChildren(noContent); - this._accessibleBuffer.focus(); - return this._bufferElementFragment; - } - let header; - let replaceChildren = true; - for (const command of commands) { - header = document.createElement('h2'); - // without this, the text area gets focused when keyboard shortcuts are used - header.tabIndex = -1; - header.textContent = command.command.replace(new RegExp(' ', 'g'), '\xA0'); - if (command.exitCode !== 0) { - header.textContent += ` exited with code ${command.exitCode}`; - } - const output = document.createElement('div'); - // without this, the text area gets focused when keyboard shortcuts are used - output.tabIndex = -1; - output.textContent = command.getOutput()?.replace(new RegExp(' ', 'g'), '\xA0') || ''; - if (replaceChildren) { - this._bufferElementFragment.replaceChildren(header, output); - replaceChildren = false; - } else { - this._bufferElementFragment.appendChild(header); - this._bufferElementFragment.appendChild(output); - } - } + async focus(): Promise { + await this._updateBufferEditor(); + // Updates xterm's accessibleBufferActive property + // such that mouse events do not cause the terminal buffer + // to steal the focus this._accessibleBuffer.focus(); - if (this._accessibleBuffer.contentEditable === 'true') { - document.execCommand('selectAll', false, undefined); - document.getSelection()?.collapseToEnd(); - } else if (header) { - // focus the cursor line's header - header.tabIndex = 0; + this._bufferEditor.focus(); + } + + private async _updateBufferEditor(): Promise { + if (!this._registered) { + // Registration is delayed until focus so the capability has time to have been added + this.add(this._terminal.raw.registerBufferElementProvider({ provideBufferElements: () => this._editorContainer })); + this._registered = true; } - return this._bufferElementFragment; + // When this is created, the element isn't yet attached so the dimensions are tiny + this._bufferEditor.layout({ width: this._accessibleBuffer.clientWidth, height: this._accessibleBuffer.clientHeight }); + const commandDetection = this._capabilities.has(TerminalCapability.CommandDetection); + const fragment = commandDetection ? this._getShellIntegrationContent() : this._getAllContent(); + const model = await this._getTextModel(URI.from({ scheme: ACCESSIBLE_BUFFER.Scheme, fragment })); + if (model) { + this._bufferEditor.setModel(model); + } + } + + async _getTextModel(resource: URI): Promise { + const existing = this._modelService.getModel(resource); + if (existing && !existing.isDisposed()) { + return existing; + } + + return this._modelService.createModel(resource.fragment, null, resource, false); + } + + private _getShellIntegrationContent(): string { + const commands = this._capabilities.get(TerminalCapability.CommandDetection)?.commands; + const sb = new StringBuilder(10000); + let content = localize('terminal.integrated.noContent', "No terminal content available for this session. Run some commands to create content."); + if (!commands?.length) { + return content; + } + for (const command of commands) { + sb.appendString(command.command.replace(new RegExp(' ', 'g'), '\xA0')); + if (command.exitCode !== 0) { + sb.appendString(` exited with code ${command.exitCode}`); + } + sb.appendString('\n'); + sb.appendString(command.getOutput()?.replace(new RegExp(' ', 'g'), '\xA0') || ''); + } + content = sb.build(); + return content; + } + + private _getAllContent(): string { + const lines: string[] = []; + let currentLine: string = ''; + const buffer = this._terminal.raw.buffer.active; + const end = buffer.length; + for (let i = 0; i < end; i++) { + const line = buffer.getLine(i); + if (!line) { + continue; + } + const isWrapped = buffer.getLine(i + 1)?.isWrapped; + currentLine += line.translateToString(!isWrapped); + if (!isWrapped || i === end - 1) { + lines.push(currentLine.replace(new RegExp(' ', 'g'), '\xA0')); + currentLine = ''; + } + } + return lines.join('\n'); } } diff --git a/src/vs/workbench/contrib/terminal/common/terminal.ts b/src/vs/workbench/contrib/terminal/common/terminal.ts index fef77887bdb..9a889cabc54 100644 --- a/src/vs/workbench/contrib/terminal/common/terminal.ts +++ b/src/vs/workbench/contrib/terminal/common/terminal.ts @@ -253,7 +253,6 @@ export interface ITerminalConfiguration { }; useWslProfiles: boolean; altClickMovesCursor: boolean; - accessibleBufferContentEditable: 'auto' | 'on' | 'off'; macOptionIsMeta: boolean; macOptionClickForcesSelection: boolean; gpuAcceleration: 'auto' | 'on' | 'canvas' | 'off'; diff --git a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts index a2dc090c6db..e07eeb45570 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts @@ -106,6 +106,11 @@ const terminalConfiguration: IConfigurationNode = { default: 'right', description: localize('terminal.integrated.tabs.location', "Controls the location of the terminal tabs, either to the left or right of the actual terminal(s).") }, + [TerminalSettingId.TabFocusMode]: { + markdownDescription: localize('tabFocusMode', "Controls whether the terminal receives tabs or defers them to the workbench for navigation. this overrides {0} when the terminal is focused.", '`#editor.tabFocusMode#`'), + type: 'boolean', + default: false + }, [TerminalSettingId.DefaultLocation]: { type: 'string', enum: [TerminalLocationString.Editor, TerminalLocationString.TerminalView], @@ -579,17 +584,6 @@ const terminalConfiguration: IConfigurationNode = { markdownDescription: localize('terminal.integrated.smoothScrolling', "Controls whether the terminal will scroll using an animation."), type: 'boolean', default: false - }, - [TerminalSettingId.AccessibleBufferContentEditable]: { - markdownDescription: localize('terminal.integrated.accessibleBufferContentEditable', "Controls whether the accessible buffer is marks as a `contenteditable` element. This adds a text cursor to the buffer, allowing selection with the keyboard without a screen reader. Screen reader users will typically want to leave this as `auto` or `off` which will treat the buffer similar to a document. By default, on Linux, this will be set to `on` so that it works when using Orca."), - type: 'string', - enum: ['auto', 'on', 'off'], - enumDescriptions: [ - localize('accessibleBufferContentEditable.auto', "Automatically enable when a screen reader is not detected."), - localize('accessibleBufferContentEditable.on', "Always on."), - localize('accessibleBufferContentEditable.off', "Always off.") - ], - default: 'auto' } } }; diff --git a/src/vs/workbench/contrib/timeline/browser/timelinePane.ts b/src/vs/workbench/contrib/timeline/browser/timelinePane.ts index bb387304c73..e1cd80f4e1a 100644 --- a/src/vs/workbench/contrib/timeline/browser/timelinePane.ts +++ b/src/vs/workbench/contrib/timeline/browser/timelinePane.ts @@ -504,7 +504,7 @@ export class TimelinePane extends ViewPane { this.pendingRequests.clear(); - if (!this.isBodyVisible()) { + if (!this.isBodyVisible() && this.tree) { this.tree.setChildren(null, undefined); this._isEmpty = true; } diff --git a/src/vs/workbench/electron-sandbox/actions/developerActions.ts b/src/vs/workbench/electron-sandbox/actions/developerActions.ts index 660ad2206e8..75959edb375 100644 --- a/src/vs/workbench/electron-sandbox/actions/developerActions.ts +++ b/src/vs/workbench/electron-sandbox/actions/developerActions.ts @@ -68,7 +68,6 @@ export class ConfigureRuntimeArgumentsAction extends Action2 { } } - export class ToggleSharedProcessAction extends Action2 { constructor() { @@ -130,5 +129,3 @@ export class OpenUserDataFolderAction extends Action2 { return nativeHostService.showItemInFolder(itemToShow.fsPath); } } - - diff --git a/src/vs/workbench/electron-sandbox/desktop.contribution.ts b/src/vs/workbench/electron-sandbox/desktop.contribution.ts index 1b9d1d905a9..1c7aa6f610a 100644 --- a/src/vs/workbench/electron-sandbox/desktop.contribution.ts +++ b/src/vs/workbench/electron-sandbox/desktop.contribution.ts @@ -267,6 +267,13 @@ import { applicationConfigurationNodeBase } from 'vs/workbench/common/configurat 'scope': ConfigurationScope.APPLICATION, ignoreSync: true }, + 'window.experimental.sharedProcessUseUtilityProcess': { // TODO@bpasero remove me once sandbox is final + type: 'boolean', + description: localize('experimentalUseSharedProcessUseUtilityProcess', "Experimental: When enabled, the window will have sandbox mode enabled via Electron API."), + default: false, //typeof product.quality === 'string' && product.quality !== 'stable', // disabled by default in stable for now + 'scope': ConfigurationScope.APPLICATION, + ignoreSync: true + } } }); diff --git a/src/vs/workbench/electron-sandbox/desktop.main.ts b/src/vs/workbench/electron-sandbox/desktop.main.ts index d01d405bb5e..ea8d8b0a613 100644 --- a/src/vs/workbench/electron-sandbox/desktop.main.ts +++ b/src/vs/workbench/electron-sandbox/desktop.main.ts @@ -49,7 +49,7 @@ import { Schemas } from 'vs/base/common/network'; import { DiskFileSystemProvider } from 'vs/workbench/services/files/electron-sandbox/diskFileSystemProvider'; import { FileUserDataProvider } from 'vs/platform/userData/common/fileUserDataProvider'; import { IUserDataProfilesService, reviveProfile } from 'vs/platform/userDataProfile/common/userDataProfile'; -import { UserDataProfilesNativeService } from 'vs/platform/userDataProfile/electron-sandbox/userDataProfile'; +import { UserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfileIpc'; import { PolicyChannelClient } from 'vs/platform/policy/common/policyIpc'; import { IPolicyService, NullPolicyService } from 'vs/platform/policy/common/policy'; import { UserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfileService'; @@ -217,10 +217,6 @@ export class DesktopMain extends Disposable { const signService = ProxyChannel.toService(mainProcessService.getChannel('sign')); serviceCollection.set(ISignService, signService); - // Remote Agent - const remoteAgentService = this._register(new RemoteAgentService(environmentService, productService, remoteAuthorityResolverService, signService, logService)); - serviceCollection.set(IRemoteAgentService, remoteAgentService); - // Files const fileService = this._register(new FileService(logService)); serviceCollection.set(IWorkbenchFileService, fileService); @@ -229,9 +225,6 @@ export class DesktopMain extends Disposable { const diskFileSystemProvider = this._register(new DiskFileSystemProvider(mainProcessService, utilityProcessWorkerWorkbenchService, logService)); fileService.registerProvider(Schemas.file, diskFileSystemProvider); - // Remote Files - this._register(RemoteFileSystemProviderClient.register(remoteAgentService, fileService, logService)); - // User Data Provider fileService.registerProvider(Schemas.vscodeUserData, this._register(new FileUserDataProvider(Schemas.file, diskFileSystemProvider, Schemas.vscodeUserData, logService))); @@ -240,11 +233,18 @@ export class DesktopMain extends Disposable { serviceCollection.set(IUriIdentityService, uriIdentityService); // User Data Profiles - const userDataProfilesService = new UserDataProfilesNativeService(this.configuration.profiles.all, mainProcessService, environmentService); + const userDataProfilesService = new UserDataProfilesService(this.configuration.profiles.all, URI.revive(this.configuration.profiles.home), mainProcessService.getChannel('userDataProfiles')); serviceCollection.set(IUserDataProfilesService, userDataProfilesService); const userDataProfileService = new UserDataProfileService(reviveProfile(this.configuration.profiles.profile, userDataProfilesService.profilesHome.scheme), userDataProfilesService); serviceCollection.set(IUserDataProfileService, userDataProfileService); + // Remote Agent + const remoteAgentService = this._register(new RemoteAgentService(userDataProfileService, environmentService, productService, remoteAuthorityResolverService, signService, logService)); + serviceCollection.set(IRemoteAgentService, remoteAgentService); + + // Remote Files + this._register(RemoteFileSystemProviderClient.register(remoteAgentService, fileService, logService)); + // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! // // NOTE: Please do NOT register services here. Use `registerSingleton()` diff --git a/src/vs/workbench/electron-sandbox/window.ts b/src/vs/workbench/electron-sandbox/window.ts index 25ad4c2fe93..4bc86f06bd8 100644 --- a/src/vs/workbench/electron-sandbox/window.ts +++ b/src/vs/workbench/electron-sandbox/window.ts @@ -69,6 +69,7 @@ import { IBannerService } from 'vs/workbench/services/banner/browser/bannerServi import { Codicon } from 'vs/base/common/codicons'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; +import { IUtilityProcessWorkerWorkbenchService } from 'vs/workbench/services/utilityProcess/electron-sandbox/utilityProcessWorkerWorkbenchService'; export class NativeWindow extends Disposable { @@ -122,7 +123,8 @@ export class NativeWindow extends Disposable { @ILabelService private readonly labelService: ILabelService, @IBannerService private readonly bannerService: IBannerService, @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, - @IPreferencesService private readonly preferencesService: IPreferencesService + @IPreferencesService private readonly preferencesService: IPreferencesService, + @IUtilityProcessWorkerWorkbenchService private readonly utilityProcessWorkerWorkbenchService: IUtilityProcessWorkerWorkbenchService ) { super(); @@ -623,7 +625,10 @@ export class NativeWindow extends Disposable { // Notify some services about lifecycle phases this.lifecycleService.when(LifecyclePhase.Ready).then(() => this.nativeHostService.notifyReady()); - this.lifecycleService.when(LifecyclePhase.Restored).then(() => this.sharedProcessService.notifyRestored()); + this.lifecycleService.when(LifecyclePhase.Restored).then(() => { + this.sharedProcessService.notifyRestored(); + this.utilityProcessWorkerWorkbenchService.notifyRestored(); + }); // Check for situations that are worth warning the user about this.handleWarnings(); diff --git a/src/vs/workbench/services/configuration/test/browser/configurationService.test.ts b/src/vs/workbench/services/configuration/test/browser/configurationService.test.ts index e55a03e2baa..d95c45d5285 100644 --- a/src/vs/workbench/services/configuration/test/browser/configurationService.test.ts +++ b/src/vs/workbench/services/configuration/test/browser/configurationService.test.ts @@ -88,7 +88,8 @@ suite('WorkspaceContextService - Folder', () => { fileService.registerProvider(Schemas.vscodeUserData, disposables.add(new FileUserDataProvider(ROOT.scheme, fileSystemProvider, Schemas.vscodeUserData, new NullLogService()))); const uriIdentityService = new UriIdentityService(fileService); const userDataProfilesService = new UserDataProfilesService(environmentService, fileService, uriIdentityService, logService); - testObject = disposables.add(new WorkspaceService({ configurationCache: new ConfigurationCache() }, environmentService, new UserDataProfileService(userDataProfilesService.defaultProfile, userDataProfilesService), userDataProfilesService, fileService, new RemoteAgentService(null, environmentService, TestProductService, new RemoteAuthorityResolverService(undefined, undefined, TestProductService, logService), new SignService(undefined), new NullLogService()), uriIdentityService, new NullLogService(), new NullPolicyService())); + const userDataProfileService = new UserDataProfileService(userDataProfilesService.defaultProfile, userDataProfilesService); + testObject = disposables.add(new WorkspaceService({ configurationCache: new ConfigurationCache() }, environmentService, userDataProfileService, userDataProfilesService, fileService, new RemoteAgentService(null, userDataProfileService, environmentService, TestProductService, new RemoteAuthorityResolverService(undefined, undefined, TestProductService, logService), new SignService(undefined), new NullLogService()), uriIdentityService, new NullLogService(), new NullPolicyService())); await (testObject).initialize(convertToWorkspacePayload(folder)); }); @@ -130,7 +131,8 @@ suite('WorkspaceContextService - Folder', () => { fileService.registerProvider(Schemas.vscodeUserData, disposables.add(new FileUserDataProvider(ROOT.scheme, fileSystemProvider, Schemas.vscodeUserData, new NullLogService()))); const uriIdentityService = new UriIdentityService(fileService); const userDataProfilesService = new UserDataProfilesService(environmentService, fileService, uriIdentityService, logService); - const testObject = disposables.add(new WorkspaceService({ configurationCache: new ConfigurationCache() }, environmentService, new UserDataProfileService(userDataProfilesService.defaultProfile, userDataProfilesService), userDataProfilesService, fileService, new RemoteAgentService(null, environmentService, TestProductService, new RemoteAuthorityResolverService(undefined, undefined, TestProductService, logService), new SignService(undefined), new NullLogService()), uriIdentityService, new NullLogService(), new NullPolicyService())); + const userDataProfileService = new UserDataProfileService(userDataProfilesService.defaultProfile, userDataProfilesService); + const testObject = disposables.add(new WorkspaceService({ configurationCache: new ConfigurationCache() }, environmentService, userDataProfileService, userDataProfilesService, fileService, new RemoteAgentService(null, userDataProfileService, environmentService, TestProductService, new RemoteAuthorityResolverService(undefined, undefined, TestProductService, logService), new SignService(undefined), new NullLogService()), uriIdentityService, new NullLogService(), new NullPolicyService())); await (testObject).initialize(convertToWorkspacePayload(folder)); const actual = testObject.getWorkspaceFolder(joinPath(folder, 'a')); @@ -152,7 +154,8 @@ suite('WorkspaceContextService - Folder', () => { fileService.registerProvider(Schemas.vscodeUserData, disposables.add(new FileUserDataProvider(ROOT.scheme, fileSystemProvider, Schemas.vscodeUserData, new NullLogService()))); const uriIdentityService = new UriIdentityService(fileService); const userDataProfilesService = new UserDataProfilesService(environmentService, fileService, uriIdentityService, logService); - const testObject = disposables.add(new WorkspaceService({ configurationCache: new ConfigurationCache() }, environmentService, new UserDataProfileService(userDataProfilesService.defaultProfile, userDataProfilesService), userDataProfilesService, fileService, new RemoteAgentService(null, environmentService, TestProductService, new RemoteAuthorityResolverService(undefined, undefined, TestProductService, logService), new SignService(undefined), new NullLogService()), uriIdentityService, new NullLogService(), new NullPolicyService())); + const userDataProfileService = new UserDataProfileService(userDataProfilesService.defaultProfile, userDataProfilesService); + const testObject = disposables.add(new WorkspaceService({ configurationCache: new ConfigurationCache() }, environmentService, userDataProfileService, userDataProfilesService, fileService, new RemoteAgentService(null, userDataProfileService, environmentService, TestProductService, new RemoteAuthorityResolverService(undefined, undefined, TestProductService, logService), new SignService(undefined), new NullLogService()), uriIdentityService, new NullLogService(), new NullPolicyService())); await (testObject).initialize(convertToWorkspacePayload(folder)); diff --git a/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts b/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts index 10ec4f9a09d..2c15566fffc 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts @@ -14,10 +14,6 @@ export type DidChangeProfileEvent = { readonly added: ILocalExtension[]; readonl export const IProfileAwareExtensionManagementService = refineServiceDecorator(IExtensionManagementService); export interface IProfileAwareExtensionManagementService extends IExtensionManagementService { - readonly onProfileAwareInstallExtension: Event; - readonly onProfileAwareDidInstallExtensions: Event; - readonly onProfileAwareUninstallExtension: Event; - readonly onProfileAwareDidUninstallExtension: Event; readonly onDidChangeProfile: Event; } @@ -58,10 +54,6 @@ export interface IWorkbenchExtensionManagementService extends IProfileAwareExten onDidInstallExtensions: Event; onUninstallExtension: Event; onDidUninstallExtension: Event; - onProfileAwareInstallExtension: Event; - onProfileAwareDidInstallExtensions: Event; - onProfileAwareUninstallExtension: Event; - onProfileAwareDidUninstallExtension: Event; onDidChangeProfile: Event; installVSIX(location: URI, manifest: IExtensionManifest, installOptions?: InstallVSIXOptions): Promise; diff --git a/src/vs/workbench/services/extensionManagement/common/extensionManagementChannelClient.ts b/src/vs/workbench/services/extensionManagement/common/extensionManagementChannelClient.ts new file mode 100644 index 00000000000..08c9ccd94dc --- /dev/null +++ b/src/vs/workbench/services/extensionManagement/common/extensionManagementChannelClient.ts @@ -0,0 +1,130 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ILocalExtension, IGalleryExtension, InstallOptions, InstallVSIXOptions, UninstallOptions, Metadata, DidUninstallExtensionEvent, InstallExtensionEvent, InstallExtensionResult, UninstallExtensionEvent } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { URI } from 'vs/base/common/uri'; +import { ExtensionIdentifier, ExtensionType, IExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; +import { ExtensionManagementChannelClient as BaseExtensionManagementChannelClient, ExtensionEventResult } from 'vs/platform/extensionManagement/common/extensionManagementIpc'; +import { IChannel } from 'vs/base/parts/ipc/common/ipc'; +import { DidChangeUserDataProfileEvent, IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; +import { Emitter } from 'vs/base/common/event'; +import { delta } from 'vs/base/common/arrays'; +import { compare } from 'vs/base/common/strings'; +import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; +import { DidChangeProfileEvent, IProfileAwareExtensionManagementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; + +export abstract class ProfileAwareExtensionManagementChannelClient extends BaseExtensionManagementChannelClient implements IProfileAwareExtensionManagementService { + + private readonly _onDidChangeProfile = this._register(new Emitter<{ readonly added: ILocalExtension[]; readonly removed: ILocalExtension[] }>()); + readonly onDidChangeProfile = this._onDidChangeProfile.event; + + constructor(channel: IChannel, + protected readonly userDataProfileService: IUserDataProfileService, + protected readonly uriIdentityService: IUriIdentityService, + ) { + super(channel); + this._register(userDataProfileService.onDidChangeCurrentProfile(e => e.join(this.whenProfileChanged(e)))); + } + + protected override fireEvent(event: Emitter, data: InstallExtensionEvent): Promise; + protected override fireEvent(event: Emitter, data: InstallExtensionResult[]): Promise; + protected override fireEvent(event: Emitter, data: UninstallExtensionEvent): Promise; + protected override fireEvent(event: Emitter, data: DidUninstallExtensionEvent): Promise; + protected override fireEvent(event: Emitter, data: ExtensionEventResult): Promise; + protected override fireEvent(event: Emitter, data: ExtensionEventResult[]): Promise; + protected override async fireEvent(arg0: any, arg1: any): Promise { + if (Array.isArray(arg1)) { + const event = arg0 as Emitter; + const data = arg1 as ExtensionEventResult[]; + const filtered = []; + for (const e of data) { + const result = this.filterEvent(e); + if (result instanceof Promise ? await result : result) { + filtered.push(e); + } + } + if (filtered.length) { + event.fire(filtered); + } + } else { + const event = arg0 as Emitter; + const data = arg1 as ExtensionEventResult; + const result = this.filterEvent(data); + if (result instanceof Promise ? await result : result) { + event.fire(data); + } + } + } + + override async install(vsix: URI, installOptions?: InstallVSIXOptions): Promise { + installOptions = { ...installOptions, profileLocation: await this.getProfileLocation(installOptions?.profileLocation) }; + return super.install(vsix, installOptions); + } + + override async installFromLocation(location: URI, profileLocation: URI): Promise { + return super.installFromLocation(location, await this.getProfileLocation(profileLocation)); + } + + override async installFromGallery(extension: IGalleryExtension, installOptions?: InstallOptions): Promise { + installOptions = { ...installOptions, profileLocation: await this.getProfileLocation(installOptions?.profileLocation) }; + return super.installFromGallery(extension, installOptions); + } + + override async uninstall(extension: ILocalExtension, options?: UninstallOptions): Promise { + options = { ...options, profileLocation: await this.getProfileLocation(options?.profileLocation) }; + return super.uninstall(extension, options); + } + + override async getInstalled(type: ExtensionType | null = null, extensionsProfileResource?: URI): Promise { + return super.getInstalled(type, await this.getProfileLocation(extensionsProfileResource)); + } + + override async updateMetadata(local: ILocalExtension, metadata: Partial, extensionsProfileResource?: URI): Promise { + return super.updateMetadata(local, metadata, await this.getProfileLocation(extensionsProfileResource)); + } + + private async whenProfileChanged(e: DidChangeUserDataProfileEvent): Promise { + const previousProfileLocation = await this.getProfileLocation(e.previous.extensionsResource); + const currentProfileLocation = await this.getProfileLocation(e.profile.extensionsResource); + + if (this.uriIdentityService.extUri.isEqual(previousProfileLocation, currentProfileLocation)) { + return; + } + + const eventData = await this.switchExtensionsProfile(previousProfileLocation, currentProfileLocation, e.preserveData); + this._onDidChangeProfile.fire(eventData); + } + + protected async switchExtensionsProfile(previousProfileLocation: URI, currentProfileLocation: URI, preserve: boolean | ExtensionIdentifier[]): Promise { + if (preserve === true) { + await this.copyExtensions(previousProfileLocation, currentProfileLocation); + return { added: [], removed: [] }; + } else { + const oldExtensions = await this.getInstalled(ExtensionType.User, previousProfileLocation); + const newExtensions = await this.getInstalled(ExtensionType.User, currentProfileLocation); + if (Array.isArray(preserve)) { + const extensionsToInstall: IExtensionIdentifier[] = []; + for (const extension of oldExtensions) { + if (preserve.some(id => ExtensionIdentifier.equals(extension.identifier.id, id)) && + !newExtensions.some(e => ExtensionIdentifier.equals(e.identifier.id, extension.identifier.id))) { + extensionsToInstall.push(extension.identifier); + } + } + if (extensionsToInstall.length) { + await this.installExtensionsFromProfile(extensionsToInstall, previousProfileLocation, currentProfileLocation); + } + } + return delta(oldExtensions, newExtensions, (a, b) => compare(`${ExtensionIdentifier.toKey(a.identifier.id)}@${a.manifest.version}`, `${ExtensionIdentifier.toKey(b.identifier.id)}@${b.manifest.version}`)); + } + } + + protected getProfileLocation(profileLocation: URI): Promise; + protected getProfileLocation(profileLocation?: URI): Promise; + protected async getProfileLocation(profileLocation?: URI): Promise { + return profileLocation ?? this.userDataProfileService.currentProfile.extensionsResource; + } + + protected abstract filterEvent(e: ExtensionEventResult): boolean | Promise; +} diff --git a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts index abab8d56829..183b40748e2 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts @@ -42,10 +42,6 @@ export class ExtensionManagementService extends Disposable implements IWorkbench readonly onUninstallExtension: Event; readonly onDidUninstallExtension: Event; readonly onDidUpdateExtensionMetadata: Event; - readonly onProfileAwareInstallExtension: Event; - readonly onProfileAwareDidInstallExtensions: Event; - readonly onProfileAwareUninstallExtension: Event; - readonly onProfileAwareDidUninstallExtension: Event; readonly onDidChangeProfile: Event; protected readonly servers: IExtensionManagementServer[] = []; @@ -81,10 +77,6 @@ export class ExtensionManagementService extends Disposable implements IWorkbench this.onUninstallExtension = this._register(this.servers.reduce((emitter: EventMultiplexer, server) => { emitter.add(Event.map(server.extensionManagementService.onUninstallExtension, e => ({ ...e, server }))); return emitter; }, new EventMultiplexer())).event; this.onDidUninstallExtension = this._register(this.servers.reduce((emitter: EventMultiplexer, server) => { emitter.add(Event.map(server.extensionManagementService.onDidUninstallExtension, e => ({ ...e, server }))); return emitter; }, new EventMultiplexer())).event; this.onDidUpdateExtensionMetadata = this._register(this.servers.reduce((emitter: EventMultiplexer, server) => { emitter.add(server.extensionManagementService.onDidUpdateExtensionMetadata); return emitter; }, new EventMultiplexer())).event; - this.onProfileAwareInstallExtension = this._register(this.servers.reduce((emitter: EventMultiplexer, server) => { emitter.add(Event.map(server.extensionManagementService.onProfileAwareInstallExtension, e => ({ ...e, server }))); return emitter; }, new EventMultiplexer())).event; - this.onProfileAwareDidInstallExtensions = this._register(this.servers.reduce((emitter: EventMultiplexer, server) => { emitter.add(server.extensionManagementService.onProfileAwareDidInstallExtensions); return emitter; }, new EventMultiplexer())).event; - this.onProfileAwareUninstallExtension = this._register(this.servers.reduce((emitter: EventMultiplexer, server) => { emitter.add(Event.map(server.extensionManagementService.onProfileAwareUninstallExtension, e => ({ ...e, server }))); return emitter; }, new EventMultiplexer())).event; - this.onProfileAwareDidUninstallExtension = this._register(this.servers.reduce((emitter: EventMultiplexer, server) => { emitter.add(Event.map(server.extensionManagementService.onProfileAwareDidUninstallExtension, e => ({ ...e, server }))); return emitter; }, new EventMultiplexer())).event; this.onDidChangeProfile = this._register(this.servers.reduce((emitter: EventMultiplexer, server) => { emitter.add(Event.map(server.extensionManagementService.onDidChangeProfile, e => ({ ...e, server }))); return emitter; }, new EventMultiplexer())).event; } @@ -529,17 +521,11 @@ export class ExtensionManagementService extends Disposable implements IWorkbench return this._targetPlatformPromise; } - async getMetadata(extension: ILocalExtension): Promise { - const server = this.getServer(extension); - if (!server) { - return undefined; - } - return server.extensionManagementService.getMetadata(extension); - } - async cleanUp(): Promise { await Promise.allSettled(this.servers.map(server => server.extensionManagementService.cleanUp())); } registerParticipant() { throw new Error('Not Supported'); } + copyExtensions(): Promise { throw new Error('Not Supported'); } + installExtensionsFromProfile(extensions: IExtensionIdentifier[], fromProfileLocation: URI, toProfileLocation: URI): Promise { throw new Error('Not Supported'); } } diff --git a/src/vs/workbench/services/extensionManagement/common/remoteExtensionManagementService.ts b/src/vs/workbench/services/extensionManagement/common/remoteExtensionManagementService.ts index 0eb60966707..3752dc0534a 100644 --- a/src/vs/workbench/services/extensionManagement/common/remoteExtensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/common/remoteExtensionManagementService.ts @@ -4,59 +4,68 @@ *--------------------------------------------------------------------------------------------*/ import { IChannel } from 'vs/base/parts/ipc/common/ipc'; -import { Event } from 'vs/base/common/event'; -import { ILocalExtension, IGalleryExtension, InstallOptions, InstallVSIXOptions, UninstallOptions } from 'vs/platform/extensionManagement/common/extensionManagement'; import { URI } from 'vs/base/common/uri'; -import { ExtensionType } from 'vs/platform/extensions/common/extensions'; -import { IProfileAwareExtensionManagementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; -import { ExtensionManagementChannelClient } from 'vs/platform/extensionManagement/common/extensionManagementIpc'; +import { DidChangeProfileEvent, IProfileAwareExtensionManagementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; +import { IRemoteUserDataProfilesService } from 'vs/workbench/services/userDataProfile/common/remoteUserDataProfiles'; +import { ProfileAwareExtensionManagementChannelClient } from 'vs/workbench/services/extensionManagement/common/extensionManagementChannelClient'; +import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; +import { ExtensionEventResult } from 'vs/platform/extensionManagement/common/extensionManagementIpc'; +import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; -export class RemoteExtensionManagementService extends ExtensionManagementChannelClient implements IProfileAwareExtensionManagementService { - - readonly onDidChangeProfile = Event.None; - get onProfileAwareInstallExtension() { return super.onInstallExtension; } - get onProfileAwareDidInstallExtensions() { return super.onDidInstallExtensions; } - get onProfileAwareUninstallExtension() { return super.onUninstallExtension; } - get onProfileAwareDidUninstallExtension() { return super.onDidUninstallExtension; } +export class RemoteExtensionManagementService extends ProfileAwareExtensionManagementChannelClient implements IProfileAwareExtensionManagementService { constructor( channel: IChannel, - @IUserDataProfilesService private readonly userDataProfileService: IUserDataProfilesService, - @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, + @IUserDataProfileService userDataProfileService: IUserDataProfileService, + @IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService, + @IRemoteUserDataProfilesService private readonly remoteUserDataProfilesService: IRemoteUserDataProfilesService, + @IUriIdentityService uriIdentityService: IUriIdentityService ) { - super(channel); + super(channel, userDataProfileService, uriIdentityService); } - override getInstalled(type: ExtensionType | null = null, profileLocation?: URI): Promise { - this.validateProfileLocation({ profileLocation }); - return super.getInstalled(type); - } - - override uninstall(extension: ILocalExtension, options?: UninstallOptions): Promise { - options = this.validateProfileLocation(options); - return super.uninstall(extension, options); - } - - override async install(vsix: URI, options?: InstallVSIXOptions): Promise { - options = this.validateProfileLocation(options); - return super.install(vsix, options); - } - - override async installFromGallery(extension: IGalleryExtension, options?: InstallOptions): Promise { - options = this.validateProfileLocation(options); - return super.installFromGallery(extension, options); - } - - private validateProfileLocation(options?: T): T | undefined { - if (options?.profileLocation) { - if (!this.uriIdentityService.extUri.isEqual(options?.profileLocation, this.userDataProfileService.defaultProfile.extensionsResource)) { - throw new Error('This opertaion is not supported in remote scenario'); - } - options = { ...options, profileLocation: undefined }; + protected async filterEvent(e: ExtensionEventResult): Promise { + if (e.applicationScoped) { + return true; } - return options; + if (!e.profileLocation && this.userDataProfileService.currentProfile.isDefault) { + return true; + } + const currentRemoteProfile = await this.remoteUserDataProfilesService.getRemoteProfile(this.userDataProfileService.currentProfile); + if (this.uriIdentityService.extUri.isEqual(currentRemoteProfile.extensionsResource, e.profileLocation)) { + return true; + } + return false; } + protected override getProfileLocation(profileLocation: URI): Promise; + protected override getProfileLocation(profileLocation?: URI): Promise; + protected override async getProfileLocation(profileLocation?: URI): Promise { + if (!profileLocation && this.userDataProfileService.currentProfile.isDefault) { + return undefined; + } + profileLocation = await super.getProfileLocation(profileLocation); + let profile = this.userDataProfilesService.profiles.find(p => this.uriIdentityService.extUri.isEqual(p.extensionsResource, profileLocation)); + if (profile) { + profile = await this.remoteUserDataProfilesService.getRemoteProfile(profile); + } else { + profile = (await this.remoteUserDataProfilesService.getRemoteProfiles()).find(p => this.uriIdentityService.extUri.isEqual(p.extensionsResource, profileLocation)); + } + return profile?.extensionsResource; + } + + protected override async switchExtensionsProfile(previousProfileLocation: URI, currentProfileLocation: URI, preserveData: boolean | ExtensionIdentifier[]): Promise { + const remoteProfiles = await this.remoteUserDataProfilesService.getRemoteProfiles(); + const previousProfile = remoteProfiles.find(p => this.uriIdentityService.extUri.isEqual(p.extensionsResource, previousProfileLocation)); + const currentProfile = remoteProfiles.find(p => this.uriIdentityService.extUri.isEqual(p.extensionsResource, currentProfileLocation)); + if (previousProfile?.id === currentProfile?.id) { + return { added: [], removed: [] }; + } + if (preserveData === true && currentProfile?.isDefault) { + preserveData = false; + } + return super.switchExtensionsProfile(previousProfileLocation, currentProfileLocation, preserveData); + } } diff --git a/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts b/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts index 0175a3edcfa..5d986edb19e 100644 --- a/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts @@ -112,9 +112,20 @@ export class WebExtensionManagementService extends AbstractExtensionManagementSe return this.install(location, { profileLocation }); } - async getMetadata(extension: ILocalExtension, profileLocation?: URI): Promise { - const scannedExtension = await this.webExtensionsScannerService.scanExistingExtension(extension.location, extension.type, profileLocation ?? this.userDataProfileService.currentProfile.extensionsResource); - return scannedExtension?.metadata; + async installExtensionsFromProfile(extensions: IExtensionIdentifier[], fromProfileLocation: URI, toProfileLocation: URI): Promise { + const result: ILocalExtension[] = []; + const extensionsToInstall = (await this.webExtensionsScannerService.scanUserExtensions(fromProfileLocation)) + .filter(e => extensions.some(id => areSameExtensions(id, e.identifier))); + if (extensionsToInstall.length) { + await Promise.allSettled(extensionsToInstall.map(async e => { + let local = await this.installFromLocation(e.location, toProfileLocation); + if (e.metadata) { + local = await this.updateMetadata(local, e.metadata, fromProfileLocation); + } + result.push(local); + })); + } + return result; } async updateMetadata(local: ILocalExtension, metadata: Partial, profileLocation?: URI): Promise { @@ -128,6 +139,10 @@ export class WebExtensionManagementService extends AbstractExtensionManagementSe return updatedLocalExtension; } + override async copyExtensions(fromProfileLocation: URI, toProfileLocation: URI): Promise { + await this.webExtensionsScannerService.copyExtensions(fromProfileLocation, toProfileLocation, e => !e.metadata?.isApplicationScoped); + } + protected override async getCompatibleVersion(extension: IGalleryExtension, sameVersion: boolean, includePreRelease: boolean): Promise { const compatibleExtension = await super.getCompatibleVersion(extension, sameVersion, includePreRelease); if (compatibleExtension) { @@ -171,7 +186,7 @@ export class WebExtensionManagementService extends AbstractExtensionManagementSe throw new Error('This should not happen'); } if (e.preserveData) { - await this.webExtensionsScannerService.copyExtensions(previousProfileLocation, currentProfileLocation, e => !e.metadata?.isApplicationScoped); + await this.copyExtensions(previousProfileLocation, currentProfileLocation); this._onDidChangeProfile.fire({ added: [], removed: [] }); } else { const oldExtensions = await this.webExtensionsScannerService.scanUserExtensions(previousProfileLocation); diff --git a/src/vs/workbench/services/extensionManagement/electron-sandbox/nativeExtensionManagementService.ts b/src/vs/workbench/services/extensionManagement/electron-sandbox/nativeExtensionManagementService.ts index ac3fd967300..a6e8652a33f 100644 --- a/src/vs/workbench/services/extensionManagement/electron-sandbox/nativeExtensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/electron-sandbox/nativeExtensionManagementService.ts @@ -4,99 +4,48 @@ *--------------------------------------------------------------------------------------------*/ import { IChannel } from 'vs/base/parts/ipc/common/ipc'; -import { IProfileAwareExtensionManagementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; -import { ExtensionManagementChannelClient } from 'vs/platform/extensionManagement/common/extensionManagementIpc'; +import { DidChangeProfileEvent, IProfileAwareExtensionManagementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { URI } from 'vs/base/common/uri'; -import { IGalleryExtension, ILocalExtension, InstallOptions, InstallVSIXOptions, Metadata, UninstallOptions } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { ExtensionIdentifier, ExtensionType } from 'vs/platform/extensions/common/extensions'; -import { Emitter, Event } from 'vs/base/common/event'; +import { ILocalExtension, InstallVSIXOptions } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; -import { delta } from 'vs/base/common/arrays'; -import { compare } from 'vs/base/common/strings'; -import { DisposableStore } from 'vs/base/common/lifecycle'; -import { DidChangeUserDataProfileEvent, IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; +import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; import { joinPath } from 'vs/base/common/resources'; -import { IExtensionsProfileScannerService } from 'vs/platform/extensionManagement/common/extensionsProfileScannerService'; import { Schemas } from 'vs/base/common/network'; import { ILogService } from 'vs/platform/log/common/log'; import { IDownloadService } from 'vs/platform/download/common/download'; import { IFileService } from 'vs/platform/files/common/files'; import { generateUuid } from 'vs/base/common/uuid'; -import { INativeEnvironmentService } from 'vs/platform/environment/common/environment'; +import { ProfileAwareExtensionManagementChannelClient } from 'vs/workbench/services/extensionManagement/common/extensionManagementChannelClient'; +import { ExtensionIdentifier, ExtensionType, isResolverExtension } from 'vs/platform/extensions/common/extensions'; +import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-sandbox/environmentService'; -export class NativeExtensionManagementService extends ExtensionManagementChannelClient implements IProfileAwareExtensionManagementService { - - private readonly disposables = this._register(new DisposableStore()); - - get onProfileAwareInstallExtension() { return super.onInstallExtension; } - override get onInstallExtension() { return Event.filter(this.onProfileAwareInstallExtension, e => this.filterEvent(e), this.disposables); } - - get onProfileAwareDidInstallExtensions() { return super.onDidInstallExtensions; } - override get onDidInstallExtensions() { - return Event.filter( - Event.map(this.onProfileAwareDidInstallExtensions, results => results.filter(e => this.filterEvent(e)), this.disposables), - results => results.length > 0, this.disposables); - } - - get onProfileAwareUninstallExtension() { return super.onUninstallExtension; } - override get onUninstallExtension() { return Event.filter(this.onProfileAwareUninstallExtension, e => this.filterEvent(e), this.disposables); } - - get onProfileAwareDidUninstallExtension() { return super.onDidUninstallExtension; } - override get onDidUninstallExtension() { return Event.filter(this.onProfileAwareDidUninstallExtension, e => this.filterEvent(e), this.disposables); } - - private readonly _onDidChangeProfile = this._register(new Emitter<{ readonly added: ILocalExtension[]; readonly removed: ILocalExtension[] }>()); - readonly onDidChangeProfile = this._onDidChangeProfile.event; +export class NativeExtensionManagementService extends ProfileAwareExtensionManagementChannelClient implements IProfileAwareExtensionManagementService { constructor( channel: IChannel, - @IUserDataProfileService private readonly userDataProfileService: IUserDataProfileService, - @IExtensionsProfileScannerService private readonly extensionsProfileScannerService: IExtensionsProfileScannerService, - @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, + @IUserDataProfileService userDataProfileService: IUserDataProfileService, + @IUriIdentityService uriIdentityService: IUriIdentityService, @IFileService private readonly fileService: IFileService, @IDownloadService private readonly downloadService: IDownloadService, - @INativeEnvironmentService private readonly nativeEnvironmentService: INativeEnvironmentService, + @INativeWorkbenchEnvironmentService private readonly nativeEnvironmentService: INativeWorkbenchEnvironmentService, @ILogService private readonly logService: ILogService, ) { - super(channel); - this._register(userDataProfileService.onDidChangeCurrentProfile(e => e.join(this.whenProfileChanged(e)))); + super(channel, userDataProfileService, uriIdentityService); } - private filterEvent({ profileLocation, applicationScoped }: { profileLocation?: URI; applicationScoped?: boolean }): boolean { + protected filterEvent({ profileLocation, applicationScoped }: { readonly profileLocation?: URI; readonly applicationScoped?: boolean }): boolean { return applicationScoped || this.uriIdentityService.extUri.isEqual(this.userDataProfileService.currentProfile.extensionsResource, profileLocation); } override async install(vsix: URI, options?: InstallVSIXOptions): Promise { const { location, cleanup } = await this.downloadVsix(vsix); try { - options = options?.profileLocation ? options : { ...options, profileLocation: this.userDataProfileService.currentProfile.extensionsResource }; return await super.install(location, options); } finally { await cleanup(); } } - override installFromGallery(extension: IGalleryExtension, installOptions?: InstallOptions): Promise { - installOptions = installOptions?.profileLocation ? installOptions : { ...installOptions, profileLocation: this.userDataProfileService.currentProfile.extensionsResource }; - return super.installFromGallery(extension, installOptions); - } - - override uninstall(extension: ILocalExtension, options?: UninstallOptions): Promise { - options = options?.profileLocation ? options : { ...options, profileLocation: this.userDataProfileService.currentProfile.extensionsResource }; - return super.uninstall(extension, options); - } - - override getInstalled(type: ExtensionType | null = null, profileLocation: URI = this.userDataProfileService.currentProfile.extensionsResource): Promise { - return super.getInstalled(type, profileLocation); - } - - override getMetadata(local: ILocalExtension, profileLocation: URI = this.userDataProfileService.currentProfile.extensionsResource): Promise { - return super.getMetadata(local, profileLocation); - } - - override updateMetadata(local: ILocalExtension, metadata: Partial, profileLocation: URI = this.userDataProfileService.currentProfile.extensionsResource): Promise { - return super.updateMetadata(local, metadata, profileLocation); - } - private async downloadVsix(vsix: URI): Promise<{ location: URI; cleanup: () => Promise }> { if (vsix.scheme === Schemas.file) { return { location: vsix, async cleanup() { } }; @@ -115,19 +64,12 @@ export class NativeExtensionManagementService extends ExtensionManagementChannel return { location, cleanup }; } - private async whenProfileChanged(e: DidChangeUserDataProfileEvent): Promise { - const oldExtensions = await super.getInstalled(ExtensionType.User, e.previous.extensionsResource); - if (e.preserveData) { - const extensions: [ILocalExtension, Metadata | undefined][] = await Promise.all(oldExtensions - .filter(e => !e.isApplicationScoped) /* remove application scoped extensions */ - .map(async e => ([e, await this.getMetadata(e)]))); - await this.extensionsProfileScannerService.addExtensionsToProfile(extensions, e.profile.extensionsResource!); - this._onDidChangeProfile.fire({ added: [], removed: [] }); - } else { - const newExtensions = await this.getInstalled(ExtensionType.User); - const { added, removed } = delta(oldExtensions, newExtensions, (a, b) => compare(`${ExtensionIdentifier.toKey(a.identifier.id)}@${a.manifest.version}`, `${ExtensionIdentifier.toKey(b.identifier.id)}@${b.manifest.version}`)); - this._onDidChangeProfile.fire({ added, removed }); + protected override async switchExtensionsProfile(previousProfileLocation: URI, currentProfileLocation: URI, preserveData: boolean | ExtensionIdentifier[]): Promise { + if (!preserveData && this.nativeEnvironmentService.remoteAuthority) { + const previousInstalledExtensions = await this.getInstalled(ExtensionType.User, previousProfileLocation); + const resolverExtension = previousInstalledExtensions.find(e => isResolverExtension(e.manifest, this.nativeEnvironmentService.remoteAuthority)); + preserveData = resolverExtension ? [new ExtensionIdentifier(resolverExtension.identifier.id)] : preserveData; } + return super.switchExtensionsProfile(previousProfileLocation, currentProfileLocation, preserveData); } - } diff --git a/src/vs/workbench/services/extensionManagement/electron-sandbox/remoteExtensionManagementService.ts b/src/vs/workbench/services/extensionManagement/electron-sandbox/remoteExtensionManagementService.ts index faaa349995f..1eed257472b 100644 --- a/src/vs/workbench/services/extensionManagement/electron-sandbox/remoteExtensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/electron-sandbox/remoteExtensionManagementService.ts @@ -4,8 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { IChannel } from 'vs/base/parts/ipc/common/ipc'; -import { Event } from 'vs/base/common/event'; -import { ILocalExtension, IGalleryExtension, IExtensionGalleryService, InstallOperation, InstallOptions, InstallVSIXOptions, ExtensionManagementError, ExtensionManagementErrorCode, UninstallOptions, Metadata } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { ILocalExtension, IGalleryExtension, IExtensionGalleryService, InstallOperation, InstallOptions, InstallVSIXOptions, ExtensionManagementError, ExtensionManagementErrorCode } from 'vs/platform/extensionManagement/common/extensionManagement'; import { URI } from 'vs/base/common/uri'; import { ExtensionType, IExtensionManifest } from 'vs/platform/extensions/common/extensions'; import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; @@ -19,20 +18,22 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { IExtensionManagementServer, IProfileAwareExtensionManagementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { Promises } from 'vs/base/common/async'; import { IExtensionManifestPropertiesService } from 'vs/workbench/services/extensions/common/extensionManifestPropertiesService'; -import { ExtensionManagementChannelClient } from 'vs/platform/extensionManagement/common/extensionManagementIpc'; import { IFileService } from 'vs/platform/files/common/files'; +import { RemoteExtensionManagementService } from 'vs/workbench/services/extensionManagement/common/remoteExtensionManagementService'; +import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; +import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; +import { IRemoteUserDataProfilesService } from 'vs/workbench/services/userDataProfile/common/remoteUserDataProfiles'; +import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; -export class NativeRemoteExtensionManagementService extends ExtensionManagementChannelClient implements IProfileAwareExtensionManagementService { - - readonly onDidChangeProfile = Event.None; - get onProfileAwareInstallExtension() { return super.onInstallExtension; } - get onProfileAwareDidInstallExtensions() { return super.onDidInstallExtensions; } - get onProfileAwareUninstallExtension() { return super.onUninstallExtension; } - get onProfileAwareDidUninstallExtension() { return super.onDidUninstallExtension; } +export class NativeRemoteExtensionManagementService extends RemoteExtensionManagementService implements IProfileAwareExtensionManagementService { constructor( channel: IChannel, private readonly localExtensionManagementServer: IExtensionManagementServer, + @IUserDataProfileService userDataProfileService: IUserDataProfileService, + @IUserDataProfilesService userDataProfilesService: IUserDataProfilesService, + @IRemoteUserDataProfilesService remoteUserDataProfilesService: IRemoteUserDataProfilesService, + @IUriIdentityService uriIdentityService: IUriIdentityService, @ILogService private readonly logService: ILogService, @IExtensionGalleryService private readonly galleryService: IExtensionGalleryService, @IConfigurationService private readonly configurationService: IConfigurationService, @@ -40,50 +41,16 @@ export class NativeRemoteExtensionManagementService extends ExtensionManagementC @IFileService private readonly fileService: IFileService, @IExtensionManifestPropertiesService private readonly extensionManifestPropertiesService: IExtensionManifestPropertiesService, ) { - super(channel); - } - - override getInstalled(type: ExtensionType | null = null, profileLocation?: URI): Promise { - if (profileLocation) { - throw new Error('Installing extensions to a specific profile is not supported in remote scenario'); - } - return super.getInstalled(type); - } - - override uninstall(extension: ILocalExtension, options?: UninstallOptions): Promise { - if (options?.profileLocation) { - throw new Error('Installing extensions to a specific profile is not supported in remote scenario'); - } - return super.uninstall(extension, options); + super(channel, userDataProfileService, userDataProfilesService, remoteUserDataProfilesService, uriIdentityService); } override async install(vsix: URI, options?: InstallVSIXOptions): Promise { - if (options?.profileLocation) { - throw new Error('Installing extensions to a specific profile is not supported in remote scenario'); - } const local = await super.install(vsix, options); await this.installUIDependenciesAndPackedExtensions(local); return local; } - override getMetadata(local: ILocalExtension, profileLocation?: URI): Promise { - if (profileLocation) { - throw new Error('Getting metadata from a specific profile is not supported in remote scenario'); - } - return super.getMetadata(local, profileLocation); - } - - override updateMetadata(local: ILocalExtension, metadata: Partial, profileLocation?: URI): Promise { - if (profileLocation) { - throw new Error('Getting metadata from a specific profile is not supported in remote scenario'); - } - return super.updateMetadata(local, metadata, profileLocation); - } - override async installFromGallery(extension: IGalleryExtension, installOptions?: InstallOptions): Promise { - if (installOptions?.profileLocation) { - throw new Error('Installing extensions to a specific profile is not supported in remote scenario'); - } const local = await this.doInstallFromGallery(extension, installOptions); await this.installUIDependenciesAndPackedExtensions(local); return local; diff --git a/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts b/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts index f298e2c0709..39f06f3c822 100644 --- a/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts +++ b/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts @@ -319,10 +319,10 @@ suite('ExtensionEnablementService Test', () => { assert.strictEqual(testObject.getEnablementState(extension), EnablementState.EnabledGlobally); }); - test('test enable an extension globally return truthy promise', () => { - return testObject.setEnablement([aLocalExtension('pub.a')], EnablementState.DisabledGlobally) - .then(() => testObject.setEnablement([aLocalExtension('pub.a')], EnablementState.EnabledGlobally)) - .then(value => assert.ok(value)); + test('test enable an extension globally return truthy promise', async () => { + await testObject.setEnablement([aLocalExtension('pub.a')], EnablementState.DisabledGlobally); + const value = await testObject.setEnablement([aLocalExtension('pub.a')], EnablementState.EnabledGlobally); + assert.strictEqual(value[0], true); }); test('test enable an extension globally triggers change event', () => { diff --git a/src/vs/workbench/services/extensions/browser/extensionService.ts b/src/vs/workbench/services/extensions/browser/extensionService.ts index ceeb59af01f..496d82be53a 100644 --- a/src/vs/workbench/services/extensions/browser/extensionService.ts +++ b/src/vs/workbench/services/extensions/browser/extensionService.ts @@ -31,6 +31,7 @@ import { IAutomatedWindow } from 'vs/platform/log/browser/log'; import { ILogService } from 'vs/platform/log/common/log'; import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; import { dedupExtensions } from 'vs/workbench/services/extensions/common/extensionsUtil'; +import { IRemoteExtensionsScannerService } from 'vs/platform/remote/common/remoteExtensionsScanner'; export class ExtensionService extends AbstractExtensionService implements IExtensionService { @@ -52,6 +53,7 @@ export class ExtensionService extends AbstractExtensionService implements IExten @IWebExtensionsScannerService private readonly _webExtensionsScannerService: IWebExtensionsScannerService, @ILogService logService: ILogService, @IRemoteAgentService remoteAgentService: IRemoteAgentService, + @IRemoteExtensionsScannerService remoteExtensionsScannerService: IRemoteExtensionsScannerService, @ILifecycleService lifecycleService: ILifecycleService, @IRemoteAuthorityResolverService private readonly _remoteAuthorityResolverService: IRemoteAuthorityResolverService, @IUserDataProfileService userDataProfileService: IUserDataProfileService, @@ -70,6 +72,7 @@ export class ExtensionService extends AbstractExtensionService implements IExten extensionManifestPropertiesService, logService, remoteAgentService, + remoteExtensionsScannerService, lifecycleService, userDataProfileService ); @@ -86,7 +89,7 @@ export class ExtensionService extends AbstractExtensionService implements IExten protected async _scanSingleExtension(extension: IExtension): Promise { if (extension.location.scheme === Schemas.vscodeRemote) { - return this._remoteAgentService.scanSingleExtension(extension.location, extension.type === ExtensionType.System); + return this._remoteExtensionsScannerService.scanSingleExtension(extension.location, extension.type === ExtensionType.System); } const scannedExtension = await this._webExtensionsScannerService.scanExistingExtension(extension.location, extension.type, this._userDataProfileService.currentProfile.extensionsResource); @@ -205,7 +208,7 @@ export class ExtensionService extends AbstractExtensionService implements IExten let [localExtensions, remoteEnv, remoteExtensions] = await Promise.all([ this._scanWebExtensions(), this._remoteAgentService.getEnvironment(), - this._remoteAgentService.scanExtensions() + this._remoteExtensionsScannerService.scanExtensions() ]); localExtensions = this._checkEnabledAndProposedAPI(localExtensions, false); remoteExtensions = this._checkEnabledAndProposedAPI(remoteExtensions, false); diff --git a/src/vs/workbench/services/extensions/common/abstractExtensionService.ts b/src/vs/workbench/services/extensions/common/abstractExtensionService.ts index f88a36f7e21..ea6247d4f43 100644 --- a/src/vs/workbench/services/extensions/common/abstractExtensionService.ts +++ b/src/vs/workbench/services/extensions/common/abstractExtensionService.ts @@ -37,6 +37,7 @@ import { ILogService } from 'vs/platform/log/common/log'; import { IExtensionHostExitInfo, IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; +import { IRemoteExtensionsScannerService } from 'vs/platform/remote/common/remoteExtensionsScanner'; const hasOwnProperty = Object.hasOwnProperty; const NO_OP_VOID_PROMISE = Promise.resolve(undefined); @@ -188,6 +189,7 @@ export abstract class AbstractExtensionService extends Disposable implements IEx @IExtensionManifestPropertiesService protected readonly _extensionManifestPropertiesService: IExtensionManifestPropertiesService, @ILogService protected readonly _logService: ILogService, @IRemoteAgentService protected readonly _remoteAgentService: IRemoteAgentService, + @IRemoteExtensionsScannerService protected readonly _remoteExtensionsScannerService: IRemoteExtensionsScannerService, @ILifecycleService private readonly _lifecycleService: ILifecycleService, @IUserDataProfileService protected readonly _userDataProfileService: IUserDataProfileService, ) { diff --git a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts index 8929645d6dd..9f06058245a 100644 --- a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts +++ b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts @@ -51,6 +51,7 @@ export const allApiProposals = Object.freeze({ portsAttributes: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.portsAttributes.d.ts', profileContentHandlers: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.profileContentHandlers.d.ts', quickDiffProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.quickDiffProvider.d.ts', + quickPickItemTooltip: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.quickPickItemTooltip.d.ts', quickPickSortByLabel: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.quickPickSortByLabel.d.ts', resolvers: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.resolvers.d.ts', scmActionButton: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.scmActionButton.d.ts', diff --git a/src/vs/workbench/services/extensions/electron-sandbox/localProcessExtensionHost.ts b/src/vs/workbench/services/extensions/electron-sandbox/localProcessExtensionHost.ts index 132cf6090d3..d8a945776a4 100644 --- a/src/vs/workbench/services/extensions/electron-sandbox/localProcessExtensionHost.ts +++ b/src/vs/workbench/services/extensions/electron-sandbox/localProcessExtensionHost.ts @@ -287,7 +287,7 @@ export class NativeLocalProcessExtensionHost implements IExtensionHost { const inspectorUrlMatch = output.data && output.data.match(/ws:\/\/([^\s]+:(\d+)\/[^\s]+)/); if (inspectorUrlMatch) { if (!this._environmentService.isBuilt && !this._isExtensionDevTestFromCli) { - console.log(`%c[Extension Host] %cdebugger inspector at chrome-devtools://devtools/bundled/inspector.html?experiments=true&v8only=true&ws=${inspectorUrlMatch[1]}`, 'color: blue', 'color:'); + console.log(`%c[Extension Host] %cdebugger inspector at devtools://devtools/bundled/inspector.html?experiments=true&v8only=true&ws=${inspectorUrlMatch[1]}`, 'color: blue', 'color:'); } if (!this._inspectPort) { this._inspectPort = Number(inspectorUrlMatch[2]); diff --git a/src/vs/workbench/services/extensions/electron-sandbox/nativeExtensionService.ts b/src/vs/workbench/services/extensions/electron-sandbox/nativeExtensionService.ts index c2276f89cde..45022c15cc3 100644 --- a/src/vs/workbench/services/extensions/electron-sandbox/nativeExtensionService.ts +++ b/src/vs/workbench/services/extensions/electron-sandbox/nativeExtensionService.ts @@ -52,6 +52,7 @@ import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/c import { LegacyNativeLocalProcessExtensionHost } from 'vs/workbench/services/extensions/electron-sandbox/nativeLocalProcessExtensionHost'; import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { process } from 'vs/base/parts/sandbox/electron-sandbox/globals'; +import { IRemoteExtensionsScannerService } from 'vs/platform/remote/common/remoteExtensionsScanner'; import { IWorkbenchIssueService } from 'vs/workbench/services/issue/common/issue'; export class NativeExtensionService extends AbstractExtensionService implements IExtensionService { @@ -77,6 +78,7 @@ export class NativeExtensionService extends AbstractExtensionService implements @IExtensionManifestPropertiesService extensionManifestPropertiesService: IExtensionManifestPropertiesService, @ILogService logService: ILogService, @IRemoteAgentService remoteAgentService: IRemoteAgentService, + @IRemoteExtensionsScannerService remoteExtensionsScannerService: IRemoteExtensionsScannerService, @ILifecycleService lifecycleService: ILifecycleService, @IRemoteAuthorityResolverService private readonly _remoteAuthorityResolverService: IRemoteAuthorityResolverService, @INativeHostService private readonly _nativeHostService: INativeHostService, @@ -100,6 +102,7 @@ export class NativeExtensionService extends AbstractExtensionService implements extensionManifestPropertiesService, logService, remoteAgentService, + remoteExtensionsScannerService, lifecycleService, userDataProfileService ); @@ -146,7 +149,7 @@ export class NativeExtensionService extends AbstractExtensionService implements protected _scanSingleExtension(extension: IExtension): Promise { if (extension.location.scheme === Schemas.vscodeRemote) { - return this._remoteAgentService.scanSingleExtension(extension.location, extension.type === ExtensionType.System); + return this._remoteExtensionsScannerService.scanSingleExtension(extension.location, extension.type === ExtensionType.System); } return this._extensionScanner.scanSingleExtension(extension.location.fsPath, extension.type === ExtensionType.System); @@ -571,7 +574,7 @@ export class NativeExtensionService extends AbstractExtensionService implements // fetch the remote environment [remoteEnv, remoteExtensions] = await Promise.all([ this._remoteAgentService.getEnvironment(), - this._remoteAgentService.scanExtensions() + this._remoteExtensionsScannerService.scanExtensions() ]); if (!remoteEnv) { diff --git a/src/vs/workbench/services/extensions/test/browser/extensionService.test.ts b/src/vs/workbench/services/extensions/test/browser/extensionService.test.ts index 88c83a94c72..7bf74c80b6d 100644 --- a/src/vs/workbench/services/extensions/test/browser/extensionService.test.ts +++ b/src/vs/workbench/services/extensions/test/browser/extensionService.test.ts @@ -29,7 +29,7 @@ import { ExtensionManifestPropertiesService, IExtensionManifestPropertiesService import { ExtensionHostKind, ExtensionRunningLocation, IExtensionHost, IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; -import { TestEnvironmentService, TestFileService, TestLifecycleService, TestRemoteAgentService, TestUserDataProfileService, TestWebExtensionsScannerService, TestWorkbenchExtensionEnablementService, TestWorkbenchExtensionManagementService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { TestEnvironmentService, TestFileService, TestLifecycleService, TestRemoteAgentService, TestRemoteExtensionsScannerService, TestUserDataProfileService, TestWebExtensionsScannerService, TestWorkbenchExtensionEnablementService, TestWorkbenchExtensionManagementService } from 'vs/workbench/test/browser/workbenchTestServices'; import { TestContextService } from 'vs/workbench/test/common/workbenchTestServices'; import { mock } from 'vs/base/test/common/mock'; import { IExtensionHostManager } from 'vs/workbench/services/extensions/common/extensionHostManager'; @@ -37,6 +37,7 @@ import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/c import { IUserDataProfilesService, UserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { UriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentityService'; +import { IRemoteExtensionsScannerService } from 'vs/platform/remote/common/remoteExtensionsScanner'; suite('BrowserExtensionService', () => { test('pickRunningLocation', () => { @@ -183,6 +184,7 @@ suite('ExtensionService', () => { [IUserDataProfilesService, UserDataProfilesService], [IUserDataProfileService, TestUserDataProfileService], [IUriIdentityService, UriIdentityService], + [IRemoteExtensionsScannerService, TestRemoteExtensionsScannerService], ]); extService = instantiationService.get(IExtensionService); }); diff --git a/src/vs/workbench/services/hover/browser/hover.ts b/src/vs/workbench/services/hover/browser/hover.ts index d5b60af6262..1f1de94425a 100644 --- a/src/vs/workbench/services/hover/browser/hover.ts +++ b/src/vs/workbench/services/hover/browser/hover.ts @@ -58,6 +58,13 @@ export interface IHoverOptions { */ target: IHoverTarget | HTMLElement; + /** + * An ID to associate with the hover to be used as an equality check. Normally when calling + * {@link IHoverService.showHover} the options object itself is used to determine if the hover + * is the same one that is already showing, when this is set, the ID will be used instead. + */ + id?: number | string; + /** * A set of actions for the hover's "status bar". */ @@ -127,8 +134,11 @@ export interface IHoverOptions { */ trapFocus?: boolean; - /** - * The container to render the hover in. + /* + * The container to pass to {@link IContextViewProvider.showContextView} which renders the hover + * in. This is particularly useful for more natural tab focusing behavior, where the hover is + * created as the next tab index after the element being hovered and/or to workaround the + * element's container hiding on `focusout`. */ container?: HTMLElement; } diff --git a/src/vs/workbench/services/hover/browser/hoverService.ts b/src/vs/workbench/services/hover/browser/hoverService.ts index c6fbd3a8d87..47127a62549 100644 --- a/src/vs/workbench/services/hover/browser/hoverService.ts +++ b/src/vs/workbench/services/hover/browser/hoverService.ts @@ -35,7 +35,7 @@ export class HoverService implements IHoverService { } showHover(options: IHoverOptions, focus?: boolean): IHoverWidget | undefined { - if (this._currentHoverOptions === options) { + if (getHoverOptionsIdentity(this._currentHoverOptions) === getHoverOptionsIdentity(options)) { return undefined; } this._currentHoverOptions = options; @@ -135,6 +135,13 @@ export class HoverService implements IHoverService { } } +function getHoverOptionsIdentity(options: IHoverOptions | undefined): IHoverOptions | number | string | undefined { + if (options === undefined) { + return undefined; + } + return options?.id ?? options; +} + class HoverContextViewDelegate implements IDelegate { get anchorPosition() { diff --git a/src/vs/workbench/contrib/localization/browser/localeService.ts b/src/vs/workbench/services/localization/browser/localeService.ts similarity index 59% rename from src/vs/workbench/contrib/localization/browser/localeService.ts rename to src/vs/workbench/services/localization/browser/localeService.ts index cc309e04387..a089f5ee7fb 100644 --- a/src/vs/workbench/contrib/localization/browser/localeService.ts +++ b/src/vs/workbench/services/localization/browser/localeService.ts @@ -4,15 +4,19 @@ *--------------------------------------------------------------------------------------------*/ import { localize } from 'vs/nls'; -import { Language } from 'vs/base/common/platform'; +import { Language, LANGUAGE_DEFAULT } from 'vs/base/common/platform'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { ILanguagePackItem } from 'vs/platform/languagePacks/common/languagePacks'; -import { ILocaleService } from 'vs/workbench/contrib/localization/common/locale'; +import { IActiveLanguagePackService, ILocaleService } from 'vs/workbench/services/localization/common/locale'; import { IHostService } from 'vs/workbench/services/host/browser/host'; import { IProductService } from 'vs/platform/product/common/productService'; +import { withNullAsUndefined } from 'vs/base/common/types'; +import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; export class WebLocaleService implements ILocaleService { declare readonly _serviceBrand: undefined; + static readonly _LOCAL_STORAGE_EXTENSION_ID_KEY = 'vscode.nls.languagePackExtensionId'; + static readonly _LOCAL_STORAGE_LOCALE_KEY = 'vscode.nls.locale'; constructor( @IDialogService private readonly dialogService: IDialogService, @@ -26,9 +30,13 @@ export class WebLocaleService implements ILocaleService { return; } if (locale) { - window.localStorage.setItem('vscode.nls.locale', locale); + window.localStorage.setItem(WebLocaleService._LOCAL_STORAGE_LOCALE_KEY, locale); + if (languagePackItem.extensionId) { + window.localStorage.setItem(WebLocaleService._LOCAL_STORAGE_EXTENSION_ID_KEY, languagePackItem.extensionId); + } } else { - window.localStorage.removeItem('vscode.nls.locale'); + window.localStorage.removeItem(WebLocaleService._LOCAL_STORAGE_LOCALE_KEY); + window.localStorage.removeItem(WebLocaleService._LOCAL_STORAGE_EXTENSION_ID_KEY); } const restartDialog = await this.dialogService.confirm({ @@ -44,7 +52,8 @@ export class WebLocaleService implements ILocaleService { } async clearLocalePreference(): Promise { - window.localStorage.removeItem('vscode.nls.locale'); + window.localStorage.removeItem(WebLocaleService._LOCAL_STORAGE_LOCALE_KEY); + window.localStorage.removeItem(WebLocaleService._LOCAL_STORAGE_EXTENSION_ID_KEY); if (Language.value() === navigator.language) { return; @@ -62,3 +71,19 @@ export class WebLocaleService implements ILocaleService { } } } + +class WebActiveLanguagePackService implements IActiveLanguagePackService { + _serviceBrand: undefined; + + async getExtensionIdProvidingCurrentLocale(): Promise { + const language = Language.value(); + if (language === LANGUAGE_DEFAULT) { + return undefined; + } + const extensionId = window.localStorage.getItem(WebLocaleService._LOCAL_STORAGE_EXTENSION_ID_KEY); + return withNullAsUndefined(extensionId); + } +} + +registerSingleton(ILocaleService, WebLocaleService, InstantiationType.Delayed); +registerSingleton(IActiveLanguagePackService, WebActiveLanguagePackService, InstantiationType.Delayed); diff --git a/src/vs/workbench/contrib/localization/common/locale.ts b/src/vs/workbench/services/localization/common/locale.ts similarity index 75% rename from src/vs/workbench/contrib/localization/common/locale.ts rename to src/vs/workbench/services/localization/common/locale.ts index cd548e5a802..f2adc50c917 100644 --- a/src/vs/workbench/contrib/localization/common/locale.ts +++ b/src/vs/workbench/services/localization/common/locale.ts @@ -13,3 +13,10 @@ export interface ILocaleService { setLocale(languagePackItem: ILanguagePackItem, skipDialog?: boolean): Promise; clearLocalePreference(): Promise; } + +export const IActiveLanguagePackService = createDecorator('activeLanguageService'); + +export interface IActiveLanguagePackService { + readonly _serviceBrand: undefined; + getExtensionIdProvidingCurrentLocale(): Promise; +} diff --git a/src/vs/workbench/contrib/localization/electron-sandbox/localeService.ts b/src/vs/workbench/services/localization/electron-sandbox/localeService.ts similarity index 79% rename from src/vs/workbench/contrib/localization/electron-sandbox/localeService.ts rename to src/vs/workbench/services/localization/electron-sandbox/localeService.ts index 829cb8ae48e..d7124758e38 100644 --- a/src/vs/workbench/contrib/localization/electron-sandbox/localeService.ts +++ b/src/vs/workbench/services/localization/electron-sandbox/localeService.ts @@ -3,15 +3,14 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Language } from 'vs/base/common/platform'; +import { Language, LANGUAGE_DEFAULT } from 'vs/base/common/platform'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { IJSONEditingService } from 'vs/workbench/services/configuration/common/jsonEditing'; -import { ILocaleService } from 'vs/workbench/contrib/localization/common/locale'; +import { IActiveLanguagePackService, ILocaleService } from 'vs/workbench/services/localization/common/locale'; import { ILanguagePackItem, ILanguagePackService } from 'vs/platform/languagePacks/common/languagePacks'; import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite'; -import { IExtensionsViewPaneContainer, VIEWLET_ID as EXTENSIONS_VIEWLET_ID } from 'vs/workbench/contrib/extensions/common/extensions'; -import { ViewContainerLocation } from 'vs/workbench/common/views'; +import { IViewPaneContainer, ViewContainerLocation } from 'vs/workbench/common/views'; import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; import { localize } from 'vs/nls'; @@ -22,8 +21,19 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic import { IHostService } from 'vs/workbench/services/host/browser/host'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IProductService } from 'vs/platform/product/common/productService'; +import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; -export class NativeLocaleService implements ILocaleService { +// duplicate of IExtensionsViewPaneContainer in contrib +interface IExtensionsViewPaneContainer extends IViewPaneContainer { + readonly searchValue: string | undefined; + search(text: string): void; + refresh(): Promise; +} + +// duplicate of VIEWLET_ID in contrib/extensions +const EXTENSIONS_VIEWLET_ID = 'workbench.view.extensions'; + +class NativeLocaleService implements ILocaleService { _serviceBrand: undefined; constructor( @@ -144,3 +154,26 @@ export class NativeLocaleService implements ILocaleService { return confirmed; } } + +// This is its own service because the localeService depends on IJSONEditingService which causes a circular dependency +// Once that's ironed out, we can fold this into the localeService. +class NativeActiveLanguagePackService implements IActiveLanguagePackService { + _serviceBrand: undefined; + + constructor( + @ILanguagePackService private readonly languagePackService: ILanguagePackService + ) { } + + async getExtensionIdProvidingCurrentLocale(): Promise { + const language = Language.value(); + if (language === LANGUAGE_DEFAULT) { + return undefined; + } + const languages = await this.languagePackService.getInstalledLanguages(); + const languagePack = languages.find(l => l.id === language); + return languagePack?.extensionId; + } +} + +registerSingleton(ILocaleService, NativeLocaleService, InstantiationType.Delayed); +registerSingleton(IActiveLanguagePackService, NativeActiveLanguagePackService, InstantiationType.Delayed); diff --git a/src/vs/workbench/services/model/common/modelService.ts b/src/vs/workbench/services/model/common/modelService.ts index 7765c84da55..570f72df42d 100644 --- a/src/vs/workbench/services/model/common/modelService.ts +++ b/src/vs/workbench/services/model/common/modelService.ts @@ -11,27 +11,19 @@ import { ILanguageService } from 'vs/editor/common/languages/language'; import { ITextResourcePropertiesService } from 'vs/editor/common/services/textResourceConfiguration'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { ILogService } from 'vs/platform/log/common/log'; -import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; import { IPathService } from 'vs/workbench/services/path/common/pathService'; -import { ILanguageFeatureDebounceService } from 'vs/editor/common/services/languageFeatureDebounce'; -import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; export class WorkbenchModelService extends ModelService { constructor( @IConfigurationService configurationService: IConfigurationService, @ITextResourcePropertiesService resourcePropertiesService: ITextResourcePropertiesService, - @IThemeService themeService: IThemeService, - @ILogService logService: ILogService, @IUndoRedoService undoRedoService: IUndoRedoService, @ILanguageConfigurationService languageConfigurationService: ILanguageConfigurationService, @ILanguageService languageService: ILanguageService, - @ILanguageFeatureDebounceService languageFeatureDebounceService: ILanguageFeatureDebounceService, - @ILanguageFeaturesService languageFeaturesService: ILanguageFeaturesService, @IPathService private readonly _pathService: IPathService, ) { - super(configurationService, resourcePropertiesService, themeService, logService, undoRedoService, languageService, languageConfigurationService, languageFeatureDebounceService, languageFeaturesService); + super(configurationService, resourcePropertiesService, undoRedoService, languageService, languageConfigurationService); } protected override _schemaShouldMaintainUndoRedoElements(resource: URI) { diff --git a/src/vs/workbench/services/quickinput/browser/quickInputService.ts b/src/vs/workbench/services/quickinput/browser/quickInputService.ts index 95122549d9f..77498318db2 100644 --- a/src/vs/workbench/services/quickinput/browser/quickInputService.ts +++ b/src/vs/workbench/services/quickinput/browser/quickInputService.ts @@ -15,9 +15,12 @@ import { QuickInputService as BaseQuickInputService } from 'vs/platform/quickinp import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { InQuickPickContextKey } from 'vs/workbench/browser/quickaccess'; +import { IHoverService } from 'vs/workbench/services/hover/browser/hover'; +import { IHoverDelegate, IHoverDelegateOptions, IHoverWidget } from 'vs/base/browser/ui/iconLabel/iconHoverDelegate'; export class QuickInputService extends BaseQuickInputService { + private readonly hoverDelegate = new QuickInputHoverDelegate(this.configurationService, this.hoverService); private readonly inQuickInputContext = InQuickPickContextKey.bindTo(this.contextKeyService); constructor( @@ -27,7 +30,8 @@ export class QuickInputService extends BaseQuickInputService { @IContextKeyService contextKeyService: IContextKeyService, @IThemeService themeService: IThemeService, @IAccessibilityService accessibilityService: IAccessibilityService, - @ILayoutService layoutService: ILayoutService + @ILayoutService layoutService: ILayoutService, + @IHoverService private readonly hoverService: IHoverService ) { super(instantiationService, contextKeyService, themeService, accessibilityService, layoutService); @@ -42,9 +46,41 @@ export class QuickInputService extends BaseQuickInputService { protected override createController(): QuickInputController { return super.createController(this.layoutService, { ignoreFocusOut: () => !this.configurationService.getValue('workbench.quickOpen.closeOnFocusLost'), - backKeybindingLabel: () => this.keybindingService.lookupKeybinding('workbench.action.quickInputBack')?.getLabel() || undefined + backKeybindingLabel: () => this.keybindingService.lookupKeybinding('workbench.action.quickInputBack')?.getLabel() || undefined, + hoverDelegate: this.hoverDelegate }); } } +class QuickInputHoverDelegate implements IHoverDelegate { + private lastHoverHideTime = 0; + readonly placement = 'element'; + + get delay() { + if (Date.now() - this.lastHoverHideTime < 200) { + return 0; // show instantly when a hover was recently shown + } + + return this.configurationService.getValue('workbench.hover.delay'); + } + + constructor( + private readonly configurationService: IConfigurationService, + private readonly hoverService: IHoverService + ) { } + + showHover(options: IHoverDelegateOptions, focus?: boolean): IHoverWidget | undefined { + return this.hoverService.showHover({ + ...options, + hideOnHover: false, + hideOnKeyDown: false, + skipFadeInAnimation: true, + }, focus); + } + + onDidHideHover(): void { + this.lastHoverHideTime = Date.now(); + } +} + registerSingleton(IQuickInputService, QuickInputService, InstantiationType.Delayed); diff --git a/src/vs/workbench/services/remote/browser/remoteAgentService.ts b/src/vs/workbench/services/remote/browser/remoteAgentService.ts index 2cdbe229327..3cc54d49eea 100644 --- a/src/vs/workbench/services/remote/browser/remoteAgentService.ts +++ b/src/vs/workbench/services/remote/browser/remoteAgentService.ts @@ -18,18 +18,20 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { IWorkbenchContribution, IWorkbenchContributionsRegistry, Extensions } from 'vs/workbench/common/contributions'; import { IHostService } from 'vs/workbench/services/host/browser/host'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; +import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; export class RemoteAgentService extends AbstractRemoteAgentService implements IRemoteAgentService { constructor( webSocketFactory: IWebSocketFactory | null | undefined, + @IUserDataProfileService userDataProfileService: IUserDataProfileService, @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, @IProductService productService: IProductService, @IRemoteAuthorityResolverService remoteAuthorityResolverService: IRemoteAuthorityResolverService, @ISignService signService: ISignService, @ILogService logService: ILogService ) { - super(new BrowserSocketFactory(webSocketFactory), environmentService, productService, remoteAuthorityResolverService, signService, logService); + super(new BrowserSocketFactory(webSocketFactory), userDataProfileService, environmentService, productService, remoteAuthorityResolverService, signService, logService); } } diff --git a/src/vs/workbench/services/remote/common/abstractRemoteAgentService.ts b/src/vs/workbench/services/remote/common/abstractRemoteAgentService.ts index 884d1d28ef7..2b0ecd843a1 100644 --- a/src/vs/workbench/services/remote/common/abstractRemoteAgentService.ts +++ b/src/vs/workbench/services/remote/common/abstractRemoteAgentService.ts @@ -17,10 +17,8 @@ import { Emitter } from 'vs/base/common/event'; import { ISignService } from 'vs/platform/sign/common/sign'; import { ILogService } from 'vs/platform/log/common/log'; import { ITelemetryData, TelemetryLevel } from 'vs/platform/telemetry/common/telemetry'; -import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { IProductService } from 'vs/platform/product/common/productService'; -import { URI } from 'vs/base/common/uri'; -import { ImplicitActivationEvents } from 'vs/platform/extensionManagement/common/implicitActivationEvents'; +import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; export abstract class AbstractRemoteAgentService extends Disposable implements IRemoteAgentService { @@ -32,6 +30,7 @@ export abstract class AbstractRemoteAgentService extends Disposable implements I constructor( socketFactory: ISocketFactory, + @IUserDataProfileService private readonly userDataProfileService: IUserDataProfileService, @IWorkbenchEnvironmentService protected readonly _environmentService: IWorkbenchEnvironmentService, @IProductService productService: IProductService, @IRemoteAuthorityResolverService private readonly _remoteAuthorityResolverService: IRemoteAuthorityResolverService, @@ -60,7 +59,7 @@ export abstract class AbstractRemoteAgentService extends Disposable implements I if (!this._environment) { this._environment = this._withChannel( async (channel, connection) => { - const env = await RemoteExtensionEnvironmentChannelClient.getEnvironmentData(channel, connection.remoteAuthority); + const env = await RemoteExtensionEnvironmentChannelClient.getEnvironmentData(channel, connection.remoteAuthority, this.userDataProfileService.currentProfile.isDefault ? undefined : this.userDataProfileService.currentProfile.id); this._remoteAuthorityResolverService._setAuthorityConnectionToken(connection.remoteAuthority, env.connectionToken); return env; }, @@ -77,37 +76,6 @@ export abstract class AbstractRemoteAgentService extends Disposable implements I ); } - whenExtensionsReady(): Promise { - return this._withChannel( - channel => RemoteExtensionEnvironmentChannelClient.whenExtensionsReady(channel), - undefined - ); - } - - scanExtensions(skipExtensions: ExtensionIdentifier[] = []): Promise { - return this._withChannel( - async (channel, connection) => { - const scannedExtensions = await RemoteExtensionEnvironmentChannelClient.scanExtensions(channel, connection.remoteAuthority, this._environmentService.extensionDevelopmentLocationURI, skipExtensions); - scannedExtensions.forEach((extension) => ImplicitActivationEvents.updateManifest(extension)); - return scannedExtensions; - }, - [] - ).then(undefined, () => []); - } - - scanSingleExtension(extensionLocation: URI, isBuiltin: boolean): Promise { - return this._withChannel( - async (channel, connection) => { - const scannedExtension = await RemoteExtensionEnvironmentChannelClient.scanSingleExtension(channel, connection.remoteAuthority, isBuiltin, extensionLocation); - if (scannedExtension !== null) { - ImplicitActivationEvents.updateManifest(scannedExtension); - } - return scannedExtension; - }, - null - ).then(undefined, () => null); - } - getDiagnosticInfo(options: IDiagnosticInfoOptions): Promise { return this._withChannel( channel => RemoteExtensionEnvironmentChannelClient.getDiagnosticInfo(channel, options), @@ -162,6 +130,7 @@ export abstract class AbstractRemoteAgentService extends Disposable implements I } return connection.withChannel('telemetry', (channel) => callback(channel, connection)); } + } class RemoteAgentConnection extends Disposable implements IRemoteAgentConnection { diff --git a/src/vs/workbench/services/remote/common/remoteAgentEnvironmentChannel.ts b/src/vs/workbench/services/remote/common/remoteAgentEnvironmentChannel.ts index 1973082e189..c5d56b6dffe 100644 --- a/src/vs/workbench/services/remote/common/remoteAgentEnvironmentChannel.ts +++ b/src/vs/workbench/services/remote/common/remoteAgentEnvironmentChannel.ts @@ -5,17 +5,18 @@ import * as platform from 'vs/base/common/platform'; import * as performance from 'vs/base/common/performance'; -import { URI, UriComponents } from 'vs/base/common/uri'; +import { URI, UriComponents, UriDto } from 'vs/base/common/uri'; import { IChannel } from 'vs/base/parts/ipc/common/ipc'; -import { IExtensionDescription, ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { IRemoteAgentEnvironment } from 'vs/platform/remote/common/remoteAgentEnvironment'; import { IDiagnosticInfoOptions, IDiagnosticInfo } from 'vs/platform/diagnostics/common/diagnostics'; import { ITelemetryData, TelemetryLevel } from 'vs/platform/telemetry/common/telemetry'; import { IExtensionHostExitInfo } from 'vs/workbench/services/remote/common/remoteAgentService'; -import { Mutable } from 'vs/base/common/types'; +import { revive } from 'vs/base/common/marshalling'; +import { IUserDataProfile } from 'vs/platform/userDataProfile/common/userDataProfile'; export interface IGetEnvironmentDataArguments { remoteAuthority: string; + profile?: string; } export interface IGetExtensionHostExitInfoArguments { @@ -23,20 +24,6 @@ export interface IGetExtensionHostExitInfoArguments { reconnectionToken: string; } -export interface IScanExtensionsArguments { - language: string; - remoteAuthority: string; - extensionDevelopmentPath: UriComponents[] | undefined; - skipExtensions: ExtensionIdentifier[]; -} - -export interface IScanSingleExtensionArguments { - language: string; - remoteAuthority: string; - isBuiltin: boolean; - extensionLocation: UriComponents; -} - export interface IRemoteAgentEnvironmentDTO { pid: number; connectionToken: string; @@ -52,13 +39,18 @@ export interface IRemoteAgentEnvironmentDTO { arch: string; marks: performance.PerformanceMark[]; useHostProxy: boolean; + profiles: { + all: UriDto; + home: UriComponents; + }; } export class RemoteExtensionEnvironmentChannelClient { - static async getEnvironmentData(channel: IChannel, remoteAuthority: string): Promise { + static async getEnvironmentData(channel: IChannel, remoteAuthority: string, profile: string | undefined): Promise { const args: IGetEnvironmentDataArguments = { - remoteAuthority + remoteAuthority, + profile }; const data = await channel.call('getEnvironmentData', args); @@ -77,7 +69,8 @@ export class RemoteExtensionEnvironmentChannelClient { os: data.os, arch: data.arch, marks: data.marks, - useHostProxy: data.useHostProxy + useHostProxy: data.useHostProxy, + profiles: revive(data.profiles) }; } @@ -89,39 +82,6 @@ export class RemoteExtensionEnvironmentChannelClient { return channel.call('getExtensionHostExitInfo', args); } - static async whenExtensionsReady(channel: IChannel): Promise { - await channel.call('whenExtensionsReady'); - } - - static async scanExtensions(channel: IChannel, remoteAuthority: string, extensionDevelopmentPath: URI[] | undefined, skipExtensions: ExtensionIdentifier[]): Promise { - const args: IScanExtensionsArguments = { - language: platform.language, - remoteAuthority, - extensionDevelopmentPath, - skipExtensions - }; - - const extensions = await channel.call('scanExtensions', args); - extensions.forEach(ext => { (ext).extensionLocation = URI.revive(ext.extensionLocation); }); - - return extensions; - } - - static async scanSingleExtension(channel: IChannel, remoteAuthority: string, isBuiltin: boolean, extensionLocation: URI): Promise { - const args: IScanSingleExtensionArguments = { - language: platform.language, - remoteAuthority, - isBuiltin, - extensionLocation - }; - - const extension = await channel.call('scanSingleExtension', args); - if (extension) { - (>extension).extensionLocation = URI.revive(extension.extensionLocation); - } - return extension; - } - static getDiagnosticInfo(channel: IChannel, options: IDiagnosticInfoOptions): Promise { return channel.call('getDiagnosticInfo', options); } diff --git a/src/vs/workbench/services/remote/common/remoteAgentService.ts b/src/vs/workbench/services/remote/common/remoteAgentService.ts index 0054090e83d..abaf81a080e 100644 --- a/src/vs/workbench/services/remote/common/remoteAgentService.ts +++ b/src/vs/workbench/services/remote/common/remoteAgentService.ts @@ -10,8 +10,6 @@ import { IDiagnosticInfoOptions, IDiagnosticInfo } from 'vs/platform/diagnostics import { Event } from 'vs/base/common/event'; import { PersistentConnectionEvent, ISocketFactory } from 'vs/platform/remote/common/remoteAgentConnection'; import { ITelemetryData, TelemetryLevel } from 'vs/platform/telemetry/common/telemetry'; -import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; -import { URI } from 'vs/base/common/uri'; export const RemoteExtensionLogFileName = 'remoteagent'; @@ -42,15 +40,6 @@ export interface IRemoteAgentService { */ getRoundTripTime(): Promise; - whenExtensionsReady(): Promise; - /** - * Scan remote extensions. - */ - scanExtensions(skipExtensions?: ExtensionIdentifier[]): Promise; - /** - * Scan a single remote extension. - */ - scanSingleExtension(extensionLocation: URI, isBuiltin: boolean): Promise; getDiagnosticInfo(options: IDiagnosticInfoOptions): Promise; updateTelemetryLevel(telemetryLevel: TelemetryLevel): Promise; logTelemetry(eventName: string, data?: ITelemetryData): Promise; diff --git a/src/vs/workbench/services/remote/common/remoteExplorerService.ts b/src/vs/workbench/services/remote/common/remoteExplorerService.ts index 159961dd545..07de8951347 100644 --- a/src/vs/workbench/services/remote/common/remoteExplorerService.ts +++ b/src/vs/workbench/services/remote/common/remoteExplorerService.ts @@ -519,9 +519,13 @@ export class TunnelModel extends Disposable { return URI.parse(`${protocol}://${localAddress}`); } - private async getStorageKey(): Promise { + private async getStorageKey(): Promise { const workspace = this.workspaceContextService.getWorkspace(); const workspaceHash = workspace.configuration ? hash(workspace.configuration.path) : (workspace.folders.length > 0 ? hash(workspace.folders[0].uri.path) : undefined); + if (workspaceHash === undefined) { + this.logService.debug('Could not get workspace hash for forwarded ports storage key.'); + return undefined; + } return `${TUNNELS_TO_RESTORE}.${this.environmentService.remoteAuthority}.${workspaceHash}`; } @@ -532,8 +536,11 @@ export class TunnelModel extends Disposable { await this.storeForwarded(); return deprecatedValue; } - - return this.storageService.get(await this.getStorageKey(), StorageScope.PROFILE); + const storageKey = await this.getStorageKey(); + if (!storageKey) { + return undefined; + } + return this.storageService.get(storageKey, StorageScope.PROFILE); } async restoreForwarded() { @@ -561,8 +568,9 @@ export class TunnelModel extends Disposable { const key = await this.getStorageKey(); this.restoreListener = this._register(this.storageService.onDidChangeValue(async (e) => { if (e.key === key) { - this.tunnelRestoreValue = Promise.resolve(this.storageService.get(await this.getStorageKey(), StorageScope.PROFILE)); + this.tunnelRestoreValue = Promise.resolve(this.storageService.get(key, StorageScope.PROFILE)); await this.restoreForwarded(); + } })); } @@ -574,7 +582,10 @@ export class TunnelModel extends Disposable { const valueToStore = JSON.stringify(Array.from(this.forwarded.values()).filter(value => value.source.source === TunnelSource.User)); if (valueToStore !== this.knownPortsRestoreValue) { this.knownPortsRestoreValue = valueToStore; - this.storageService.store(await this.getStorageKey(), this.knownPortsRestoreValue, StorageScope.PROFILE, StorageTarget.USER); + const key = await this.getStorageKey(); + if (key) { + this.storageService.store(key, this.knownPortsRestoreValue, StorageScope.PROFILE, StorageTarget.USER); + } } } } diff --git a/src/vs/workbench/services/remote/common/remoteExtensionsScanner.ts b/src/vs/workbench/services/remote/common/remoteExtensionsScanner.ts new file mode 100644 index 00000000000..58843aa305f --- /dev/null +++ b/src/vs/workbench/services/remote/common/remoteExtensionsScanner.ts @@ -0,0 +1,89 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; +import { IRemoteExtensionsScannerService, RemoteExtensionsScannerChannelName } from 'vs/platform/remote/common/remoteExtensionsScanner'; +import * as platform from 'vs/base/common/platform'; +import { IChannel } from 'vs/base/parts/ipc/common/ipc'; +import { IExtensionDescription, IRelaxedExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { URI } from 'vs/base/common/uri'; +import { ImplicitActivationEvents } from 'vs/platform/extensionManagement/common/implicitActivationEvents'; +import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; +import { IRemoteUserDataProfilesService } from 'vs/workbench/services/userDataProfile/common/remoteUserDataProfiles'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { ILogService } from 'vs/platform/log/common/log'; +import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { IActiveLanguagePackService } from 'vs/workbench/services/localization/common/locale'; + +class RemoteExtensionsScannerService implements IRemoteExtensionsScannerService { + + declare readonly _serviceBrand: undefined; + + constructor( + @IRemoteAgentService private readonly remoteAgentService: IRemoteAgentService, + @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, + @IUserDataProfileService private readonly userDataProfileService: IUserDataProfileService, + @IRemoteUserDataProfilesService private readonly remoteUserDataProfilesService: IRemoteUserDataProfilesService, + @ILogService private readonly logService: ILogService, + @IActiveLanguagePackService private readonly activeLanguagePackService: IActiveLanguagePackService + ) { } + + whenExtensionsReady(): Promise { + return this.withChannel( + channel => channel.call('whenExtensionsReady'), + undefined + ); + } + + async scanExtensions(): Promise { + try { + const languagePack = await this.activeLanguagePackService.getExtensionIdProvidingCurrentLocale(); + return await this.withChannel( + async (channel) => { + const profileLocation = this.userDataProfileService.currentProfile.isDefault ? undefined : (await this.remoteUserDataProfilesService.getRemoteProfile(this.userDataProfileService.currentProfile)).extensionsResource; + const scannedExtensions = await channel.call('scanExtensions', [platform.language, profileLocation, this.environmentService.extensionDevelopmentLocationURI, languagePack]); + scannedExtensions.forEach((extension) => { + extension.extensionLocation = URI.revive(extension.extensionLocation); + ImplicitActivationEvents.updateManifest(extension); + }); + return scannedExtensions; + }, + [] + ); + } catch (error) { + this.logService.error(error); + return []; + } + } + + async scanSingleExtension(extensionLocation: URI, isBuiltin: boolean): Promise { + try { + return await this.withChannel( + async (channel) => { + const extension = await channel.call('scanSingleExtension', [extensionLocation, isBuiltin, platform.language]); + if (extension !== null) { + extension.extensionLocation = URI.revive(extension.extensionLocation); + ImplicitActivationEvents.updateManifest(extension); + } + return extension; + }, + null + ); + } catch (error) { + this.logService.error(error); + return null; + } + } + + private withChannel(callback: (channel: IChannel) => Promise, fallback: R): Promise { + const connection = this.remoteAgentService.getConnection(); + if (!connection) { + return Promise.resolve(fallback); + } + return connection.withChannel(RemoteExtensionsScannerChannelName, (channel) => callback(channel)); + } +} + +registerSingleton(IRemoteExtensionsScannerService, RemoteExtensionsScannerService, InstantiationType.Delayed); diff --git a/src/vs/workbench/services/remote/electron-sandbox/remoteAgentService.ts b/src/vs/workbench/services/remote/electron-sandbox/remoteAgentService.ts index 997cfc7cda5..4ab21828920 100644 --- a/src/vs/workbench/services/remote/electron-sandbox/remoteAgentService.ts +++ b/src/vs/workbench/services/remote/electron-sandbox/remoteAgentService.ts @@ -20,16 +20,18 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { INativeHostService } from 'vs/platform/native/electron-sandbox/native'; import { URI } from 'vs/base/common/uri'; import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; export class RemoteAgentService extends AbstractRemoteAgentService implements IRemoteAgentService { constructor( + @IUserDataProfileService userDataProfileService: IUserDataProfileService, @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, @IProductService productService: IProductService, @IRemoteAuthorityResolverService remoteAuthorityResolverService: IRemoteAuthorityResolverService, @ISignService signService: ISignService, @ILogService logService: ILogService, ) { - super(new BrowserSocketFactory(null), environmentService, productService, remoteAuthorityResolverService, signService, logService); + super(new BrowserSocketFactory(null), userDataProfileService, environmentService, productService, remoteAuthorityResolverService, signService, logService); } } diff --git a/src/vs/workbench/services/sharedProcess/electron-sandbox/sharedProcessService.ts b/src/vs/workbench/services/sharedProcess/electron-sandbox/sharedProcessService.ts index 6590bc91938..ce810b44b05 100644 --- a/src/vs/workbench/services/sharedProcess/electron-sandbox/sharedProcessService.ts +++ b/src/vs/workbench/services/sharedProcess/electron-sandbox/sharedProcessService.ts @@ -38,6 +38,7 @@ export class SharedProcessService extends Disposable implements ISharedProcessSe // as a result. As such, make sure we await the `Restored` // phase before making a connection attempt, but also add a // timeout to be safe against possible deadlocks. + await Promise.race([this.restoredBarrier.wait(), timeout(2000)]); // Acquire a message port connected to the shared process diff --git a/src/vs/workbench/services/textfile/browser/textFileService.ts b/src/vs/workbench/services/textfile/browser/textFileService.ts index 88f8842da6f..a78fc891cf9 100644 --- a/src/vs/workbench/services/textfile/browser/textFileService.ts +++ b/src/vs/workbench/services/textfile/browser/textFileService.ts @@ -591,13 +591,18 @@ export abstract class AbstractTextFileService extends Disposable implements ITex // of untitled model if it is a valid path name and // figure out the file extension from the mode if any. + let nameCandidate: string; if (await this.pathService.hasValidBasename(joinPath(defaultFilePath, model.name), model.name)) { - const languageId = model.getLanguageId(); - if (languageId && languageId !== PLAINTEXT_LANGUAGE_ID) { - suggestedFilename = this.suggestFilename(languageId, model.name); - } else { - suggestedFilename = model.name; - } + nameCandidate = model.name; + } else { + nameCandidate = basename(resource); + } + + const languageId = model.getLanguageId(); + if (languageId && languageId !== PLAINTEXT_LANGUAGE_ID) { + suggestedFilename = this.suggestFilename(languageId, nameCandidate); + } else { + suggestedFilename = nameCandidate; } } } diff --git a/src/vs/workbench/services/userDataProfile/common/remoteUserDataProfiles.ts b/src/vs/workbench/services/userDataProfile/common/remoteUserDataProfiles.ts new file mode 100644 index 00000000000..d3ef612836b --- /dev/null +++ b/src/vs/workbench/services/userDataProfile/common/remoteUserDataProfiles.ts @@ -0,0 +1,180 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable } from 'vs/base/common/lifecycle'; +import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { DidChangeProfilesEvent, IUserDataProfile, IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; +import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; +import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; +import { IStringDictionary } from 'vs/base/common/collections'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; +import { distinct } from 'vs/base/common/arrays'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { UserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfileIpc'; + +const associatedRemoteProfilesKey = 'associatedRemoteProfiles'; + +export const IRemoteUserDataProfilesService = createDecorator('IRemoteUserDataProfilesService'); +export interface IRemoteUserDataProfilesService { + readonly _serviceBrand: undefined; + getRemoteProfiles(): Promise; + getRemoteProfile(localProfile: IUserDataProfile): Promise; +} + +class RemoteUserDataProfilesService extends Disposable implements IRemoteUserDataProfilesService { + + readonly _serviceBrand: undefined; + + private readonly initPromise: Promise; + + private remoteUserDataProfilesService: IUserDataProfilesService | undefined; + + constructor( + @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, + @IRemoteAgentService private readonly remoteAgentService: IRemoteAgentService, + @IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService, + @IUserDataProfileService private readonly userDataProfileService: IUserDataProfileService, + @IStorageService private readonly storageService: IStorageService, + @ILogService private readonly logService: ILogService, + ) { + super(); + this.initPromise = this.init(); + } + + private async init(): Promise { + const connection = this.remoteAgentService.getConnection(); + if (!connection) { + return; + } + + const environment = await this.remoteAgentService.getEnvironment(); + if (!environment) { + return; + } + + this.remoteUserDataProfilesService = new UserDataProfilesService(environment.profiles.all, environment.profiles.home, connection.getChannel('userDataProfiles')); + this._register(this.userDataProfilesService.onDidChangeProfiles(e => this.onDidChangeLocalProfiles(e))); + + // Associate current local profile with remote profile + const remoteProfile = await this.getAssociatedRemoteProfile(this.userDataProfileService.currentProfile, this.remoteUserDataProfilesService); + if (!remoteProfile.isDefault) { + this.setAssociatedRemoteProfiles([...this.getAssociatedRemoteProfiles(), remoteProfile.id]); + } + + this.cleanUp(); + } + + private async onDidChangeLocalProfiles(e: DidChangeProfilesEvent): Promise { + for (const profile of e.removed) { + const remoteProfile = this.remoteUserDataProfilesService?.profiles.find(p => p.id === profile.id); + if (remoteProfile) { + await this.remoteUserDataProfilesService?.removeProfile(remoteProfile); + } + } + } + + async getRemoteProfiles(): Promise { + await this.initPromise; + + if (!this.remoteUserDataProfilesService) { + throw new Error('Remote profiles service not available in the current window'); + } + + return this.remoteUserDataProfilesService.profiles; + } + + async getRemoteProfile(localProfile: IUserDataProfile): Promise { + await this.initPromise; + + if (!this.remoteUserDataProfilesService) { + throw new Error('Remote profiles service not available in the current window'); + } + + return this.getAssociatedRemoteProfile(localProfile, this.remoteUserDataProfilesService); + } + + private async getAssociatedRemoteProfile(localProfile: IUserDataProfile, remoteUserDataProfilesService: IUserDataProfilesService): Promise { + // If the local profile is the default profile, return the remote default profile + if (localProfile.isDefault) { + return remoteUserDataProfilesService.defaultProfile; + } + + let profile = remoteUserDataProfilesService.profiles.find(p => p.id === localProfile.id); + if (!profile) { + profile = await remoteUserDataProfilesService.createProfile(localProfile.id, localProfile.name, { + shortName: localProfile.shortName, + transient: localProfile.isTransient, + useDefaultFlags: localProfile.useDefaultFlags, + }); + this.setAssociatedRemoteProfiles([...this.getAssociatedRemoteProfiles(), this.userDataProfileService.currentProfile.id]); + } + return profile; + } + + private getAssociatedRemoteProfiles(): string[] { + if (this.environmentService.remoteAuthority) { + const remotes = this.parseAssociatedRemoteProfiles(); + return remotes[this.environmentService.remoteAuthority] ?? []; + } + return []; + } + + private setAssociatedRemoteProfiles(profiles: string[]): void { + if (this.environmentService.remoteAuthority) { + const remotes = this.parseAssociatedRemoteProfiles(); + profiles = distinct(profiles); + if (profiles.length) { + remotes[this.environmentService.remoteAuthority] = profiles; + } else { + delete remotes[this.environmentService.remoteAuthority]; + } + if (Object.keys(remotes).length) { + this.storageService.store(associatedRemoteProfilesKey, JSON.stringify(remotes), StorageScope.APPLICATION, StorageTarget.MACHINE); + } else { + this.storageService.remove(associatedRemoteProfilesKey, StorageScope.APPLICATION); + } + } + } + + private parseAssociatedRemoteProfiles(): IStringDictionary { + if (this.environmentService.remoteAuthority) { + const value = this.storageService.get(associatedRemoteProfilesKey, StorageScope.APPLICATION); + try { + return value ? JSON.parse(value) : {}; + } catch (error) { + this.logService.error(error); + } + } + return {}; + } + + private async cleanUp(): Promise { + const associatedRemoteProfiles: string[] = []; + for (const profileId of this.getAssociatedRemoteProfiles()) { + const remoteProfile = this.remoteUserDataProfilesService?.profiles.find(p => p.id === profileId); + if (!remoteProfile) { + continue; + } + const localProfile = this.userDataProfilesService.profiles.find(p => p.id === profileId); + if (localProfile) { + if (localProfile.name !== remoteProfile.name || localProfile.shortName !== remoteProfile.shortName) { + await this.remoteUserDataProfilesService?.updateProfile(remoteProfile, { name: localProfile.name, shortName: localProfile.shortName }); + } + associatedRemoteProfiles.push(profileId); + continue; + } + if (remoteProfile) { + // Cleanup remote profiles those are not available locally + await this.remoteUserDataProfilesService?.removeProfile(remoteProfile); + } + } + this.setAssociatedRemoteProfiles(associatedRemoteProfiles); + } + +} + +registerSingleton(IRemoteUserDataProfilesService, RemoteUserDataProfilesService, InstantiationType.Delayed); diff --git a/src/vs/workbench/services/utilityProcess/electron-sandbox/utilityProcessWorkerWorkbenchService.ts b/src/vs/workbench/services/utilityProcess/electron-sandbox/utilityProcessWorkerWorkbenchService.ts index c507be2d617..801b5d805fc 100644 --- a/src/vs/workbench/services/utilityProcess/electron-sandbox/utilityProcessWorkerWorkbenchService.ts +++ b/src/vs/workbench/services/utilityProcess/electron-sandbox/utilityProcessWorkerWorkbenchService.ts @@ -12,6 +12,7 @@ import { IPCClient, ProxyChannel } from 'vs/base/parts/ipc/common/ipc'; import { generateUuid } from 'vs/base/common/uuid'; import { acquirePort } from 'vs/base/parts/ipc/electron-sandbox/ipc.mp'; import { IOnDidTerminateUtilityrocessWorkerProcess, ipcUtilityProcessWorkerChannelName, IUtilityProcessWorkerProcess, IUtilityProcessWorkerService } from 'vs/platform/utilityProcess/common/utilityProcessWorkerService'; +import { Barrier, timeout } from 'vs/base/common/async'; export const IUtilityProcessWorkerWorkbenchService = createDecorator('utilityProcessWorkerWorkbenchService'); @@ -61,6 +62,11 @@ export interface IUtilityProcessWorkerWorkbenchService { * allows to terminate the worker if needed. */ createWorker(process: IUtilityProcessWorkerProcess): Promise; + + /** + * Notifies the service that the workbench window has restored. + */ + notifyRestored(): void; } export class UtilityProcessWorkerWorkbenchService extends Disposable implements IUtilityProcessWorkerWorkbenchService { @@ -77,6 +83,8 @@ export class UtilityProcessWorkerWorkbenchService extends Disposable implements return this._utilityProcessWorkerService; } + private readonly restoredBarrier = new Barrier(); + constructor( readonly windowId: number, private readonly useUtilityProcess: boolean, @@ -90,6 +98,13 @@ export class UtilityProcessWorkerWorkbenchService extends Disposable implements async createWorker(process: IUtilityProcessWorkerProcess): Promise { this.logService.trace('Renderer->UtilityProcess#createWorker'); + // We want to avoid heavy utility process work to happen before + // the window has restored. As such, make sure we await the + // `Restored` phase before making a connection attempt, but also + // add a timeout to be safe against possible deadlocks. + + await Promise.race([this.restoredBarrier.wait(), timeout(2000)]); + // Get ready to acquire the message port from the utility process worker const nonce = generateUuid(); const responseChannel = 'vscode:createUtilityProcessWorkerMessageChannelResult'; @@ -119,4 +134,10 @@ export class UtilityProcessWorkerWorkbenchService extends Disposable implements return { client, onDidTerminate, dispose: () => disposables.dispose() }; } + + notifyRestored(): void { + if (!this.restoredBarrier.isOpen()) { + this.restoredBarrier.open(); + } + } } diff --git a/src/vs/workbench/test/browser/parts/editor/editorGroupModel.test.ts b/src/vs/workbench/test/browser/parts/editor/editorGroupModel.test.ts index 33e0061620d..1b70d16389c 100644 --- a/src/vs/workbench/test/browser/parts/editor/editorGroupModel.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/editorGroupModel.test.ts @@ -2124,7 +2124,7 @@ suite('EditorGroupModel', () => { assert.strictEqual(group.indexOf(input2), 1); assert.strictEqual(group.indexOf(input3), 2); - group.moveEditor(input1, 2); // moved out of sticky range + group.moveEditor(input1, 2); // moved out of sticky range// assert.strictEqual(group.isSticky(input1), false); assert.strictEqual(group.isSticky(input2), true); assert.strictEqual(group.isSticky(input3), false); @@ -2171,7 +2171,7 @@ suite('EditorGroupModel', () => { assert.strictEqual(group.indexOf(input2), 1); assert.strictEqual(group.indexOf(input3), 2); - group.moveEditor(input3, 0); // moved into sticky range + group.moveEditor(input3, 0); // moved into sticky range// assert.strictEqual(group.isSticky(input1), true); assert.strictEqual(group.isSticky(input2), false); assert.strictEqual(group.isSticky(input3), true); @@ -2325,4 +2325,40 @@ suite('EditorGroupModel', () => { assert.strictEqual(group2Events.opened[1].editor, input2group2); assert.strictEqual(group2Events.opened[1].editorIndex, 1); }); + + test('moving editor sends sticky event when sticky changes', () => { + const group1 = createEditorGroupModel(); + + const input1group1 = input(); + const input2group1 = input(); + const input3group1 = input(); + + // Open all the editors + group1.openEditor(input1group1, { pinned: true, active: true, index: 0, sticky: true }); + group1.openEditor(input2group1, { pinned: true, active: false, index: 1 }); + group1.openEditor(input3group1, { pinned: true, active: false, index: 2 }); + + const group1Events = groupListener(group1); + + group1.moveEditor(input2group1, 0); + assert.strictEqual(group1Events.sticky[0].editor, input2group1); + assert.strictEqual(group1Events.sticky[0].editorIndex, 0); + + const group2 = createEditorGroupModel(); + + const input1group2 = input(); + const input2group2 = input(); + const input3group2 = input(); + + // Open all the editors + group2.openEditor(input1group2, { pinned: true, active: true, index: 0, sticky: true }); + group2.openEditor(input2group2, { pinned: true, active: false, index: 1 }); + group2.openEditor(input3group2, { pinned: true, active: false, index: 2 }); + + const group2Events = groupListener(group2); + + group2.moveEditor(input1group2, 1); + assert.strictEqual(group2Events.unsticky[0].editor, input1group2); + assert.strictEqual(group2Events.unsticky[0].editorIndex, 1); + }); }); diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts index 078dd9d4f38..cbae6e46829 100644 --- a/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -155,7 +155,7 @@ import { TestEditorWorkerService } from 'vs/editor/test/common/services/testEdit import { IExtensionHostExitInfo, IRemoteAgentConnection, IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; import { ILanguageDetectionService } from 'vs/workbench/services/languageDetection/common/languageDetectionWorkerService'; import { IDiagnosticInfoOptions, IDiagnosticInfo } from 'vs/platform/diagnostics/common/diagnostics'; -import { ExtensionIdentifier, ExtensionType, IExtension, IExtensionDescription, IRelaxedExtensionManifest, TargetPlatform } from 'vs/platform/extensions/common/extensions'; +import { ExtensionType, IExtension, IExtensionDescription, IRelaxedExtensionManifest, TargetPlatform } from 'vs/platform/extensions/common/extensions'; import { ISocketFactory } from 'vs/platform/remote/common/remoteAgentConnection'; import { IRemoteAgentEnvironment } from 'vs/platform/remote/common/remoteAgentEnvironment'; import { ILayoutOffsetInfo } from 'vs/platform/layout/browser/layoutService'; @@ -165,6 +165,8 @@ import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/c import { EnablementState, IExtensionManagementServer, IScannedExtension, IWebExtensionsScannerService, IWorkbenchExtensionEnablementService, IWorkbenchExtensionManagementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { InstallVSIXOptions, ILocalExtension, IGalleryExtension, InstallOptions, IExtensionIdentifier, UninstallOptions, IExtensionsControlManifest, IGalleryMetadata, IExtensionManagementParticipant, Metadata } from 'vs/platform/extensionManagement/common/extensionManagement'; import { Codicon } from 'vs/base/common/codicons'; +import { IHoverOptions, IHoverService, IHoverWidget } from 'vs/workbench/services/hover/browser/hover'; +import { IRemoteExtensionsScannerService } from 'vs/platform/remote/common/remoteExtensionsScanner'; export function createFileEditorInput(instantiationService: IInstantiationService, resource: URI): FileEditorInput { return instantiationService.createInstance(FileEditorInput, resource, undefined, undefined, undefined, undefined, undefined, undefined); @@ -316,7 +318,8 @@ export function workbenchInstantiationService( instantiationService.stub(ICodeEditorService, disposables.add(new CodeEditorService(editorService, themeService, configService))); instantiationService.stub(IPaneCompositePartService, new TestPaneCompositeService()); instantiationService.stub(IListService, new TestListService()); - instantiationService.stub(IQuickInputService, disposables.add(new QuickInputService(configService, instantiationService, keybindingService, contextKeyService, themeService, accessibilityService, layoutService))); + const hoverService = instantiationService.stub(IHoverService, instantiationService.createInstance(TestHoverService)); + instantiationService.stub(IQuickInputService, disposables.add(new QuickInputService(configService, instantiationService, keybindingService, contextKeyService, themeService, accessibilityService, layoutService, hoverService))); instantiationService.stub(IWorkspacesService, new TestWorkspacesService()); instantiationService.stub(IWorkspaceTrustManagementService, new TestWorkspaceTrustManagementService()); instantiationService.stub(ITerminalInstanceService, new TestTerminalInstanceService()); @@ -729,6 +732,24 @@ export class TestSideBarPart implements IPaneCompositePart { layout(width: number, height: number, top: number, left: number): void { } } +class TestHoverService implements IHoverService { + private currentHover: IHoverWidget | undefined; + _serviceBrand: undefined; + showHover(options: IHoverOptions, focus?: boolean | undefined): IHoverWidget | undefined { + this.currentHover = new class implements IHoverWidget { + private _isDisposed = false; + get isDisposed(): boolean { return this._isDisposed; } + dispose(): void { + this._isDisposed = true; + } + }; + return this.currentHover; + } + hideHover(): void { + this.currentHover?.dispose(); + } +} + export class TestPanelPart implements IPaneCompositePart, IPaneCompositeSelectorPart { declare readonly _serviceBrand: undefined; @@ -1920,9 +1941,6 @@ export class TestRemoteAgentService implements IRemoteAgentService { async getEnvironment(): Promise { return null; } async getRawEnvironment(): Promise { return null; } async getExtensionHostExitInfo(reconnectionToken: string): Promise { return null; } - async whenExtensionsReady(): Promise { } - scanExtensions(skipExtensions?: ExtensionIdentifier[]): Promise { throw new Error('Method not implemented.'); } - scanSingleExtension(extensionLocation: URI, isBuiltin: boolean): Promise { throw new Error('Method not implemented.'); } async getDiagnosticInfo(options: IDiagnosticInfoOptions): Promise { return undefined; } async updateTelemetryLevel(telemetryLevel: TelemetryLevel): Promise { } async logTelemetry(eventName: string, data?: ITelemetryData): Promise { } @@ -1930,6 +1948,13 @@ export class TestRemoteAgentService implements IRemoteAgentService { async getRoundTripTime(): Promise { return undefined; } } +export class TestRemoteExtensionsScannerService implements IRemoteExtensionsScannerService { + declare readonly _serviceBrand: undefined; + async whenExtensionsReady(): Promise { } + scanExtensions(): Promise { throw new Error('Method not implemented.'); } + scanSingleExtension(): Promise { throw new Error('Method not implemented.'); } +} + export class TestWorkbenchExtensionEnablementService implements IWorkbenchExtensionEnablementService { _serviceBrand: undefined; onEnablementChanged = Event.None; @@ -1996,9 +2021,6 @@ export class TestWorkbenchExtensionManagementService implements IWorkbenchExtens getExtensionsControlManifest(): Promise { throw new Error('Method not implemented.'); } - getMetadata(extension: ILocalExtension): Promise | undefined> { - throw new Error('Method not implemented.'); - } async updateMetadata(local: ILocalExtension, metadata: Partial): Promise { return local; } registerParticipant(pariticipant: IExtensionManagementParticipant): void { } async getTargetPlatform(): Promise { return TargetPlatform.UNDEFINED; } @@ -2006,6 +2028,8 @@ export class TestWorkbenchExtensionManagementService implements IWorkbenchExtens download(): Promise { throw new Error('Method not implemented.'); } + copyExtensions(): Promise { throw new Error('Not Supported'); } + installExtensionsFromProfile(): Promise { throw new Error('Not Supported'); } } export class TestUserDataProfileService implements IUserDataProfileService { diff --git a/src/vs/workbench/test/electron-browser/workbenchTestServices.ts b/src/vs/workbench/test/electron-browser/workbenchTestServices.ts index a2a0e92435f..2bb2ada89e6 100644 --- a/src/vs/workbench/test/electron-browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/electron-browser/workbenchTestServices.ts @@ -87,7 +87,7 @@ export const TestNativeWindowConfiguration: INativeWindowConfiguration = { homeDir: homeDir, tmpDir: tmpdir(), userDataDir: getUserDataPath(args, product.nameShort), - profiles: { profile: NULL_PROFILE, all: [NULL_PROFILE] }, + profiles: { profile: NULL_PROFILE, all: [NULL_PROFILE], home: URI.file(homeDir) }, preferUtilityProcess: false, ...args }; diff --git a/src/vs/workbench/workbench.common.main.ts b/src/vs/workbench/workbench.common.main.ts index de62b1c5cf6..d25396579f5 100644 --- a/src/vs/workbench/workbench.common.main.ts +++ b/src/vs/workbench/workbench.common.main.ts @@ -86,7 +86,9 @@ import 'vs/workbench/services/notification/common/notificationService'; import 'vs/workbench/services/userDataSync/common/userDataSyncUtil'; import 'vs/workbench/services/userDataProfile/browser/userDataProfileImportExportService'; import 'vs/workbench/services/userDataProfile/browser/userDataProfileManagement'; +import 'vs/workbench/services/userDataProfile/common/remoteUserDataProfiles'; import 'vs/workbench/services/remote/common/remoteExplorerService'; +import 'vs/workbench/services/remote/common/remoteExtensionsScanner'; import 'vs/workbench/services/workingCopy/common/workingCopyService'; import 'vs/workbench/services/workingCopy/common/workingCopyFileService'; import 'vs/workbench/services/workingCopy/common/workingCopyEditorService'; @@ -100,6 +102,7 @@ import 'vs/workbench/services/assignment/common/assignmentService'; import 'vs/workbench/services/outline/browser/outlineService'; import 'vs/workbench/services/languageDetection/browser/languageDetectionWorkerServiceImpl'; import 'vs/editor/common/services/languageFeaturesService'; +import 'vs/editor/common/services/semanticTokensStylingService'; import 'vs/workbench/services/textMate/browser/textMateTokenizationFeature.contribution'; import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; diff --git a/src/vs/workbench/workbench.desktop.main.ts b/src/vs/workbench/workbench.desktop.main.ts index aca9e38ac7a..421408a15b3 100644 --- a/src/vs/workbench/workbench.desktop.main.ts +++ b/src/vs/workbench/workbench.desktop.main.ts @@ -61,6 +61,7 @@ import 'vs/workbench/services/localization/electron-sandbox/languagePackService' import 'vs/workbench/services/telemetry/electron-sandbox/telemetryService'; import 'vs/workbench/services/extensions/electron-sandbox/extensionHostStarter'; import 'vs/platform/extensionResourceLoader/common/extensionResourceLoaderService'; +import 'vs/workbench/services/localization/electron-sandbox/localeService'; import 'vs/platform/extensionManagement/electron-sandbox/extensionsScannerService'; import 'vs/workbench/services/extensionManagement/electron-sandbox/extensionManagementServerService'; import 'vs/workbench/services/extensionManagement/electron-sandbox/extensionTipsService'; diff --git a/src/vs/workbench/workbench.web.main.ts b/src/vs/workbench/workbench.web.main.ts index 7069ed5ba89..20b0040085a 100644 --- a/src/vs/workbench/workbench.web.main.ts +++ b/src/vs/workbench/workbench.web.main.ts @@ -52,6 +52,7 @@ import 'vs/workbench/services/dialogs/browser/fileDialogService'; import 'vs/workbench/services/host/browser/browserHostService'; import 'vs/workbench/services/lifecycle/browser/lifecycleService'; import 'vs/workbench/services/clipboard/browser/clipboardService'; +import 'vs/workbench/services/localization/browser/localeService'; import 'vs/workbench/services/path/browser/pathService'; import 'vs/workbench/services/themes/browser/browserHostColorSchemeService'; import 'vs/workbench/services/encryption/browser/encryptionService'; @@ -179,6 +180,7 @@ import { Event, Emitter } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import { GroupOrientation } from 'vs/workbench/services/editor/common/editorGroupsService'; import { UserDataSyncResourceProviderService } from 'vs/platform/userDataSync/common/userDataSyncResourceProvider'; +import { RemoteAuthorityResolverError } from 'vs/platform/remote/common/remoteAuthorityResolver'; export { @@ -192,6 +194,7 @@ export { Disposable, GroupOrientation, LogLevel, + RemoteAuthorityResolverError, // Facade API env, diff --git a/src/vscode-dts/vscode.proposed.quickDiffProvider.d.ts b/src/vscode-dts/vscode.proposed.quickDiffProvider.d.ts index c0c0078f561..3778e7092eb 100644 --- a/src/vscode-dts/vscode.proposed.quickDiffProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.quickDiffProvider.d.ts @@ -10,4 +10,8 @@ declare module 'vscode' { export namespace window { export function registerQuickDiffProvider(selector: DocumentSelector, quickDiffProvider: QuickDiffProvider, label: string, rootUri?: Uri): Disposable; } + + interface QuickDiffProvider { + label?: string; + } } diff --git a/src/vscode-dts/vscode.proposed.quickPickItemTooltip.d.ts b/src/vscode-dts/vscode.proposed.quickPickItemTooltip.d.ts new file mode 100644 index 00000000000..4e7d00fa5ed --- /dev/null +++ b/src/vscode-dts/vscode.proposed.quickPickItemTooltip.d.ts @@ -0,0 +1,16 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + + // https://github.com/microsoft/vscode/issues/73904 + + export interface QuickPickItem { + /** + * An optional flag to sort the final results by index of first query match in label. Defaults to true. + */ + tooltip?: string | MarkdownString; + } +} diff --git a/test/automation/src/problems.ts b/test/automation/src/problems.ts index 0527b50d767..026ccf4b061 100644 --- a/test/automation/src/problems.ts +++ b/test/automation/src/problems.ts @@ -33,7 +33,7 @@ export class Problems { static getSelectorInProblemsView(problemType: ProblemSeverity): string { const selector = problemType === ProblemSeverity.WARNING ? 'codicon-warning' : 'codicon-error'; - return `div[id="workbench.panel.markers"] .monaco-tl-contents .marker-icon.${selector}`; + return `div[id="workbench.panel.markers"] .monaco-tl-contents .marker-icon .${selector}`; } static getSelectorInEditor(problemType: ProblemSeverity): string { diff --git a/yarn.lock b/yarn.lock index d7a63b6d240..e4e5585ac46 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1233,6 +1233,14 @@ xml2js "^0.4.23" yargs "^17.5.1" +"@vscode/policy-watcher@^1.1.4": + version "1.1.4" + resolved "https://registry.yarnpkg.com/@vscode/policy-watcher/-/policy-watcher-1.1.4.tgz#a47e30c6f02d025187d8900a3e21adb2626dece5" + integrity sha512-/xYsB7PmEeEpuRIKPAqDvK8baJ6AfOhHsnPPJrje7Bpf1z2GLwRFq7pm9KjBGtcwJGInuDxQERMtX0RIERi8YA== + dependencies: + bindings "^1.5.0" + node-addon-api "^6.0.0" + "@vscode/ripgrep@^1.14.2": version "1.14.2" resolved "https://registry.yarnpkg.com/@vscode/ripgrep/-/ripgrep-1.14.2.tgz#47c0eec2b64f53d8f7e1b5ffd22a62e229191c34" @@ -7752,11 +7760,6 @@ node-abi@^3.3.0: dependencies: semver "^7.3.5" -node-addon-api@*: - version "5.0.0" - resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-5.0.0.tgz#7d7e6f9ef89043befdb20c1989c905ebde18c501" - integrity sha512-CvkDw2OEnme7ybCykJpVcKH+uAOLV2qLqiyla128dN9TkEWfrYmxG6C2boDe5KcNQqZF3orkqzGgOMvZ/JNekA== - node-addon-api@^3.0.2: version "3.1.0" resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.1.0.tgz#98b21931557466c6729e51cb77cd39c965f42239" @@ -7777,6 +7780,11 @@ node-addon-api@^4.3.0: resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-4.3.0.tgz#52a1a0b475193e0928e98e0426a0d1254782b77f" integrity sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ== +node-addon-api@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-6.0.0.tgz#cfb3574e6df708ff71a30db6c4762d9e06e11c27" + integrity sha512-GyHvgPvUXBvAkXa0YvYnhilSB1A+FRYMpIVggKzPZqdaZfevZOuzfWzyvgzOwRLHBeo/MMswmJFsrNF4Nw1pmA== + node-fetch@2.6.8: version "2.6.8" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.8.tgz#a68d30b162bc1d8fd71a367e81b997e1f4d4937e" @@ -11425,14 +11433,6 @@ vscode-oniguruma@1.7.0: resolved "https://registry.yarnpkg.com/vscode-oniguruma/-/vscode-oniguruma-1.7.0.tgz#439bfad8fe71abd7798338d1cd3dc53a8beea94b" integrity sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA== -vscode-policy-watcher@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/vscode-policy-watcher/-/vscode-policy-watcher-1.1.1.tgz#3646d17de9f82c741437fd6d687fd0c414ea513d" - integrity sha512-ZwJKg8gpIQ3UO8gdTqBvbXJBOBUStWPArjQJuqGIz8TimgMy+4H7tScVjvlxmzb/sS6rErn3wGMn8gRH824nnw== - dependencies: - bindings "^1.5.0" - node-addon-api "*" - vscode-proxy-agent@^0.12.0: version "0.12.0" resolved "https://registry.yarnpkg.com/vscode-proxy-agent/-/vscode-proxy-agent-0.12.0.tgz#0775f464b9519b0c903da4dcf50851e1453f4e48" @@ -11863,15 +11863,15 @@ xterm-addon-webgl@0.15.0-beta.7: resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.15.0-beta.7.tgz#ab247b499f61e8eebff92e08ec5ca999d87e06af" integrity sha512-7WCI/D6uFNp3y9TeTsbSo1h7gCy4h/yP2lWn8ZEjCaiGvO11DbKMq17fbiwaR3YmGWXoRKkcLaNIiqxFnjKO4w== -xterm-headless@5.2.0-beta.28: - version "5.2.0-beta.28" - resolved "https://registry.yarnpkg.com/xterm-headless/-/xterm-headless-5.2.0-beta.28.tgz#d2c149da51ef138f46268b755c4fdc4202eb771c" - integrity sha512-4XcjBhFwuyjpz2ubESwp75UceySOOKdJszKyyxOQ3/7L937uiVEBBLc8T231XU8lSwWUU7czyNjYyCfpszY4+Q== +xterm-headless@5.2.0-beta.29: + version "5.2.0-beta.29" + resolved "https://registry.yarnpkg.com/xterm-headless/-/xterm-headless-5.2.0-beta.29.tgz#dd08312fdb4292c217e685d9e2e8b1957364e298" + integrity sha512-1P4urIeDTkl2C+zGb4WUnKJMACZMPGYHwVXMjkB0WhMISbkt6M34MH9ljxHhnL99dHwlx2Lvi6wvhnpyZucWCg== -xterm@5.2.0-beta.28: - version "5.2.0-beta.28" - resolved "https://registry.yarnpkg.com/xterm/-/xterm-5.2.0-beta.28.tgz#852347e4eaf5aae7d82c90592a42adc9daab2a79" - integrity sha512-aLDxCuqjWHjvnhfWfkxy/y6coNrC+QIhbDe2sdfLPkrxhK6KnYE6qiZD5jXUIQXeq0KmSDcYi/esuKujvobC2A== +xterm@5.2.0-beta.29: + version "5.2.0-beta.29" + resolved "https://registry.yarnpkg.com/xterm/-/xterm-5.2.0-beta.29.tgz#99764aff5cd8cdb4335f5d59466b134cfcb45e3e" + integrity sha512-zx5RKcQqo78bza4R/m3WtxAJCBAF4U61fy6cxqb1PkqXF9/qdYlySUCVOauMxv+6n6cAxt3EQWwLlgvbvQBbsw== y18n@^3.2.1: version "3.2.2"