Merge remote-tracking branch 'origin/main' into rebornix/nb-list-focus

This commit is contained in:
rebornix 2021-03-12 10:59:40 -08:00
commit 216bff5771
No known key found for this signature in database
GPG key ID: 181FC90D15393C20
80 changed files with 1770 additions and 1579 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -4,12 +4,18 @@
*--------------------------------------------------------------------------------------------*/
import type * as markdownIt from 'markdown-it';
import 'katex/dist/katex.min.css';
declare const extendMarkdownIt: undefined | (
(f: (md: markdownIt.MarkdownIt) => void) => void
);
const styleHref = (document.currentScript as any).src.replace('notebook-out/katex.js', '') + 'node_modules/katex/dist/katex.min.css';
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = styleHref;
document.head.append(link);
(function () {
const katex = require('@iktakahiro/markdown-it-katex');
if (typeof extendMarkdownIt !== 'undefined') {

View file

@ -39,11 +39,7 @@
},
"devDependencies": {
"@types/markdown-it": "^0.0.0",
"css-loader": "^5.0.2",
"markdown-it": "^12.0.4",
"null-loader": "^4.0.1",
"style-loader": "^2.0.0",
"url-loader": "^4.1.1"
"markdown-it": "^12.0.4"
},
"repository": {
"type": "git",

View file

@ -14,19 +14,6 @@ module.exports = {
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/
},
{
test: /\.css$/i,
use: ['style-loader', 'css-loader'],
},
{
test: /\.(woff2)$/i,
use: ['url-loader?limit=100000']
},
{
test: /\.(woff|eot|ttf|otf)$/i,
use: ['null-loader']
},
],
},

View file

@ -9,121 +9,26 @@
dependencies:
katex "^0.12.0"
"@types/json-schema@^7.0.6":
version "7.0.7"
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.7.tgz#98a993516c859eb0d5c4c8f098317a9ea68db9ad"
integrity sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA==
"@types/markdown-it@^0.0.0":
version "0.0.0"
resolved "https://registry.yarnpkg.com/@types/markdown-it/-/markdown-it-0.0.0.tgz#8f6acaa5e3245e275f684e95deb3e518d1c6ab16"
integrity sha1-j2rKpeMkXidfaE6V3rPlGNHGqxY=
ajv-keywords@^3.5.2:
version "3.5.2"
resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d"
integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==
ajv@^6.12.5:
version "6.12.6"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==
dependencies:
fast-deep-equal "^3.1.1"
fast-json-stable-stringify "^2.0.0"
json-schema-traverse "^0.4.1"
uri-js "^4.2.2"
argparse@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38"
integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==
big.js@^5.2.2:
version "5.2.2"
resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328"
integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==
camelcase@^6.2.0:
version "6.2.0"
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.2.0.tgz#924af881c9d525ac9d87f40d964e5cea982a1809"
integrity sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==
colorette@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.1.tgz#4d0b921325c14faf92633086a536db6e89564b1b"
integrity sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw==
commander@^2.19.0:
version "2.20.3"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
css-loader@^5.0.2:
version "5.0.2"
resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-5.0.2.tgz#24f758dae349bad0a440c50d7e2067742e0899cb"
integrity sha512-gbkBigdcHbmNvZ1Cg6aV6qh6k9N6XOr8YWzISLQGrwk2mgOH8LLrizhkxbDhQtaLtktyKHD4970S0xwz5btfTA==
dependencies:
camelcase "^6.2.0"
cssesc "^3.0.0"
icss-utils "^5.1.0"
loader-utils "^2.0.0"
postcss "^8.2.4"
postcss-modules-extract-imports "^3.0.0"
postcss-modules-local-by-default "^4.0.0"
postcss-modules-scope "^3.0.0"
postcss-modules-values "^4.0.0"
postcss-value-parser "^4.1.0"
schema-utils "^3.0.0"
semver "^7.3.4"
cssesc@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee"
integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==
emojis-list@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78"
integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==
entities@~2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/entities/-/entities-2.1.0.tgz#992d3129cf7df6870b96c57858c249a120f8b8b5"
integrity sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==
fast-deep-equal@^3.1.1:
version "3.1.3"
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
fast-json-stable-stringify@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633"
integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==
icss-utils@^5.0.0, icss-utils@^5.1.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-5.1.0.tgz#c6be6858abd013d768e98366ae47e25d5887b1ae"
integrity sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==
indexes-of@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607"
integrity sha1-8w9xbI4r00bHtn0985FVZqfAVgc=
json-schema-traverse@^0.4.1:
version "0.4.1"
resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660"
integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==
json5@^2.1.2:
version "2.2.0"
resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.0.tgz#2dfefe720c6ba525d9ebd909950f0515316c89a3"
integrity sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==
dependencies:
minimist "^1.2.5"
katex@^0.12.0:
version "0.12.0"
resolved "https://registry.yarnpkg.com/katex/-/katex-0.12.0.tgz#2fb1c665dbd2b043edcf8a1f5c555f46beaa0cb9"
@ -138,22 +43,6 @@ linkify-it@^3.0.1:
dependencies:
uc.micro "^1.0.1"
loader-utils@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.0.tgz#e4cace5b816d425a166b5f097e10cd12b36064b0"
integrity sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==
dependencies:
big.js "^5.2.2"
emojis-list "^3.0.0"
json5 "^2.1.2"
lru-cache@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94"
integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==
dependencies:
yallist "^4.0.0"
markdown-it-emoji@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/markdown-it-emoji/-/markdown-it-emoji-2.0.0.tgz#3164ad4c009efd946e98274f7562ad611089a231"
@ -175,154 +64,7 @@ mdurl@^1.0.1:
resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e"
integrity sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4=
mime-db@1.45.0:
version "1.45.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.45.0.tgz#cceeda21ccd7c3a745eba2decd55d4b73e7879ea"
integrity sha512-CkqLUxUk15hofLoLyljJSrukZi8mAtgd+yE5uO4tqRZsdsAJKv0O+rFMhVDRJgozy+yG6md5KwuXhD4ocIoP+w==
mime-types@^2.1.27:
version "2.1.28"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.28.tgz#1160c4757eab2c5363888e005273ecf79d2a0ecd"
integrity sha512-0TO2yJ5YHYr7M2zzT7gDU1tbwHxEUWBCLt0lscSNpcdAfFyJOVEpRYNS7EXVcTLNj/25QO8gulHC5JtTzSE2UQ==
dependencies:
mime-db "1.45.0"
minimist@^1.2.5:
version "1.2.5"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
nanoid@^3.1.20:
version "3.1.20"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.20.tgz#badc263c6b1dcf14b71efaa85f6ab4c1d6cfc788"
integrity sha512-a1cQNyczgKbLX9jwbS/+d7W8fX/RfgYR7lVWwWOGIPNgK2m0MWvrGF6/m4kk6U3QcFMnZf3RIhL0v2Jgh/0Uxw==
null-loader@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/null-loader/-/null-loader-4.0.1.tgz#8e63bd3a2dd3c64236a4679428632edd0a6dbc6a"
integrity sha512-pxqVbi4U6N26lq+LmgIbB5XATP0VdZKOG25DhHi8btMmJJefGArFyDg1yc4U3hWCJbMqSrw0qyrz1UQX+qYXqg==
dependencies:
loader-utils "^2.0.0"
schema-utils "^3.0.0"
postcss-modules-extract-imports@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz#cda1f047c0ae80c97dbe28c3e76a43b88025741d"
integrity sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==
postcss-modules-local-by-default@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.0.tgz#ebbb54fae1598eecfdf691a02b3ff3b390a5a51c"
integrity sha512-sT7ihtmGSF9yhm6ggikHdV0hlziDTX7oFoXtuVWeDd3hHObNkcHRo9V3yg7vCAY7cONyxJC/XXCmmiHHcvX7bQ==
dependencies:
icss-utils "^5.0.0"
postcss-selector-parser "^6.0.2"
postcss-value-parser "^4.1.0"
postcss-modules-scope@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz#9ef3151456d3bbfa120ca44898dfca6f2fa01f06"
integrity sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==
dependencies:
postcss-selector-parser "^6.0.4"
postcss-modules-values@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz#d7c5e7e68c3bb3c9b27cbf48ca0bb3ffb4602c9c"
integrity sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==
dependencies:
icss-utils "^5.0.0"
postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4:
version "6.0.4"
resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.4.tgz#56075a1380a04604c38b063ea7767a129af5c2b3"
integrity sha512-gjMeXBempyInaBqpp8gODmwZ52WaYsVOsfr4L4lDQ7n3ncD6mEyySiDtgzCT+NYC0mmeOLvtsF8iaEf0YT6dBw==
dependencies:
cssesc "^3.0.0"
indexes-of "^1.0.1"
uniq "^1.0.1"
util-deprecate "^1.0.2"
postcss-value-parser@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz#443f6a20ced6481a2bda4fa8532a6e55d789a2cb"
integrity sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==
postcss@^8.2.4:
version "8.2.6"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.2.6.tgz#5d69a974543b45f87e464bc4c3e392a97d6be9fe"
integrity sha512-xpB8qYxgPuly166AGlpRjUdEYtmOWx2iCwGmrv4vqZL9YPVviDVPZPRXxnXr6xPZOdxQ9lp3ZBFCRgWJ7LE3Sg==
dependencies:
colorette "^1.2.1"
nanoid "^3.1.20"
source-map "^0.6.1"
punycode@^2.1.0:
version "2.1.1"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
schema-utils@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.0.0.tgz#67502f6aa2b66a2d4032b4279a2944978a0913ef"
integrity sha512-6D82/xSzO094ajanoOSbe4YvXWMfn2A//8Y1+MUqFAJul5Bs+yn36xbK9OtNDcRVSBJ9jjeoXftM6CfztsjOAA==
dependencies:
"@types/json-schema" "^7.0.6"
ajv "^6.12.5"
ajv-keywords "^3.5.2"
semver@^7.3.4:
version "7.3.4"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.4.tgz#27aaa7d2e4ca76452f98d3add093a72c943edc97"
integrity sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==
dependencies:
lru-cache "^6.0.0"
source-map@^0.6.1:
version "0.6.1"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
style-loader@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-2.0.0.tgz#9669602fd4690740eaaec137799a03addbbc393c"
integrity sha512-Z0gYUJmzZ6ZdRUqpg1r8GsaFKypE+3xAzuFeMuoHgjc9KZv3wMyCRjQIWEbhoFSq7+7yoHXySDJyyWQaPajeiQ==
dependencies:
loader-utils "^2.0.0"
schema-utils "^3.0.0"
uc.micro@^1.0.1, uc.micro@^1.0.5:
version "1.0.6"
resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac"
integrity sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==
uniq@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/uniq/-/uniq-1.0.1.tgz#b31c5ae8254844a3a8281541ce2b04b865a734ff"
integrity sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=
uri-js@^4.2.2:
version "4.4.1"
resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e"
integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==
dependencies:
punycode "^2.1.0"
url-loader@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/url-loader/-/url-loader-4.1.1.tgz#28505e905cae158cf07c92ca622d7f237e70a4e2"
integrity sha512-3BTV812+AVHHOJQO8O5MkWgZ5aosP7GnROJwvzLS9hWDj00lZ6Z0wNak423Lp9PBZN05N+Jk/N5Si8jRAlGyWA==
dependencies:
loader-utils "^2.0.0"
mime-types "^2.1.27"
schema-utils "^3.0.0"
util-deprecate@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
yallist@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==

View file

@ -683,6 +683,7 @@ class TypeFilterController<T, TFilterData> implements IDisposable {
if (typeof options.filterOnType !== 'undefined') {
this._filterOnType = !!options.filterOnType;
this.filterOnTypeDomNode.checked = this._filterOnType;
this.updateFilterOnTypeTitleAndIcon();
}
if (typeof options.automaticKeyboardNavigation !== 'undefined') {

View file

@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { homedir } from 'os';
import { constants, existsSync, statSync, unlinkSync, chmodSync, truncateSync, readFileSync } from 'fs';
import { existsSync, statSync, unlinkSync, chmodSync, truncateSync, readFileSync } from 'fs';
import { spawn, ChildProcess, SpawnOptions } from 'child_process';
import { buildHelpMessage, buildVersionMessage, OPTIONS } from 'vs/platform/environment/node/argv';
import { NativeParsedArgs } from 'vs/platform/environment/common/argv';
@ -84,8 +84,8 @@ export async function main(argv: string[]): Promise<any> {
let restoreMode = false;
if (!!args['file-chmod']) {
targetMode = statSync(target).mode;
if (!(targetMode & constants.S_IWUSR)) {
chmodSync(target, targetMode | constants.S_IWUSR);
if (!(targetMode & 0o200 /* File mode indicating writable by owner */)) {
chmodSync(target, targetMode | 0o200);
restoreMode = true;
}
}

View file

@ -741,6 +741,19 @@ export class ModelSemanticColoring extends Disposable {
this._fetchDocumentSemanticTokens.schedule();
}
}));
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 = [];

View file

@ -346,38 +346,48 @@ export class RenderLineOutput {
export function renderViewLine(input: RenderLineInput, sb: IStringBuilder): RenderLineOutput {
if (input.lineContent.length === 0) {
let containsForeignElements = ForeignElementType.None;
let content: string = '<span><span></span></span>';
if (input.lineDecorations.length > 0) {
// This line is empty, but it contains inline decorations
const beforeClassNames: string[] = [];
const afterClassNames: string[] = [];
for (let i = 0, len = input.lineDecorations.length; i < len; i++) {
const lineDecoration = input.lineDecorations[i];
if (lineDecoration.type === InlineDecorationType.Before) {
beforeClassNames.push(input.lineDecorations[i].className);
containsForeignElements |= ForeignElementType.Before;
}
if (lineDecoration.type === InlineDecorationType.After) {
afterClassNames.push(input.lineDecorations[i].className);
containsForeignElements |= ForeignElementType.After;
sb.appendASCIIString(`<span>`);
let beforeCount = 0;
let afterCount = 0;
let containsForeignElements = ForeignElementType.None;
for (const lineDecoration of input.lineDecorations) {
if (lineDecoration.type === InlineDecorationType.Before || lineDecoration.type === InlineDecorationType.After) {
sb.appendASCIIString(`<span class="`);
sb.appendASCIIString(lineDecoration.className);
sb.appendASCIIString(`"></span>`);
if (lineDecoration.type === InlineDecorationType.Before) {
containsForeignElements |= ForeignElementType.Before;
beforeCount++;
}
if (lineDecoration.type === InlineDecorationType.After) {
containsForeignElements |= ForeignElementType.After;
afterCount++;
}
}
}
if (containsForeignElements !== ForeignElementType.None) {
const beforeSpan = (beforeClassNames.length > 0 ? `<span class="${beforeClassNames.join(' ')}"></span>` : ``);
const afterSpan = (afterClassNames.length > 0 ? `<span class="${afterClassNames.join(' ')}"></span>` : ``);
content = `<span>${beforeSpan}${afterSpan}</span>`;
}
sb.appendASCIIString(`</span>`);
const characterMapping = new CharacterMapping(1, beforeCount + afterCount);
characterMapping.setPartData(0, beforeCount, 0, 0);
return new RenderLineOutput(
characterMapping,
false,
containsForeignElements
);
}
sb.appendASCIIString(content);
// completely empty line
sb.appendASCIIString('<span><span></span></span>');
return new RenderLineOutput(
new CharacterMapping(0, 0),
false,
containsForeignElements
ForeignElementType.None
);
}

View file

@ -320,7 +320,7 @@ export class ReferenceWidget extends peekView.PeekViewWidget {
keyboardNavigationLabelProvider: this._instantiationService.createInstance(StringRepresentationProvider),
identityProvider: new IdentityProvider(),
openOnSingleClick: true,
openOnFocus: true,
selectionNavigation: true,
overrideStyles: {
listBackground: peekView.peekViewResultsBackground
}

View file

@ -1643,8 +1643,8 @@ suite('viewLineRenderer.renderLine 2', () => {
0,
createViewLineTokens([createPart(0, 3)]),
[
new LineDecoration(1, 2, 'before', InlineDecorationType.Before),
new LineDecoration(0, 1, 'after', InlineDecorationType.After),
new LineDecoration(1, 1, 'before', InlineDecorationType.Before),
new LineDecoration(1, 1, 'after', InlineDecorationType.After),
],
2,
0,
@ -1668,6 +1668,47 @@ suite('viewLineRenderer.renderLine 2', () => {
assert.deepStrictEqual(actual.html, expected);
});
test('issue #118759: enable multiple text editor decorations in empty lines', () => {
let actual = renderViewLine(new RenderLineInput(
true,
true,
'',
false,
true,
false,
0,
createViewLineTokens([createPart(0, 3)]),
[
new LineDecoration(1, 1, 'after1', InlineDecorationType.After),
new LineDecoration(1, 1, 'after2', InlineDecorationType.After),
new LineDecoration(1, 1, 'before1', InlineDecorationType.Before),
new LineDecoration(1, 1, 'before2', InlineDecorationType.Before),
],
2,
0,
10,
10,
10,
10000,
'none',
false,
false,
null
));
let expected = [
'<span>',
'<span class="before1"></span>',
'<span class="before2"></span>',
'<span class="after1"></span>',
'<span class="after2"></span>',
'</span>'
].join('');
assert.deepStrictEqual(actual.html, expected);
});
test('issue #38935: GitLens end-of-line blame no longer rendering', () => {
let actual = renderViewLine(new RenderLineInput(

View file

@ -41,6 +41,7 @@ export interface NativeParsedArgs {
'builtin-extensions-dir'?: string;
extensionDevelopmentPath?: string[]; // // undefined or array of 1 or more local paths or URIs
extensionTestsPath?: string; // either a local path or a URI
extensionDevelopmentKind?: string[];
'inspect-extensions'?: string;
'inspect-brk-extensions'?: string;
debugId?: string;

View file

@ -6,6 +6,7 @@
import { createDecorator, refineServiceDecorator } from 'vs/platform/instantiation/common/instantiation';
import { URI } from 'vs/base/common/uri';
import { NativeParsedArgs } from 'vs/platform/environment/common/argv';
import { ExtensionKind } from 'vs/platform/extensions/common/extensions';
export const IEnvironmentService = createDecorator<IEnvironmentService>('environmentService');
export const INativeEnvironmentService = refineServiceDecorator<IEnvironmentService, INativeEnvironmentService>(IEnvironmentService);
@ -62,6 +63,7 @@ export interface IEnvironmentService {
isExtensionDevelopment: boolean;
disableExtensions: boolean | string[];
extensionDevelopmentLocationURI?: URI[];
extensionDevelopmentKind?: ExtensionKind[];
extensionTestsLocationURI?: URI;
// --- logging

View file

@ -83,6 +83,7 @@ export const OPTIONS: OptionDescriptions<Required<NativeParsedArgs>> = {
'locate-extension': { type: 'string[]' },
'extensionDevelopmentPath': { type: 'string[]' },
'extensionDevelopmentKind': { type: 'string[]' },
'extensionTestsPath': { type: 'string' },
'debugId': { type: 'string' },
'debugRenderer': { type: 'boolean' },

View file

@ -14,6 +14,7 @@ import { memoize } from 'vs/base/common/decorators';
import { toLocalISOString } from 'vs/base/common/date';
import { FileAccess } from 'vs/base/common/network';
import { URI } from 'vs/base/common/uri';
import { ExtensionKind } from 'vs/platform/extensions/common/extensions';
export class NativeEnvironmentService implements INativeEnvironmentService {
@ -151,6 +152,11 @@ export class NativeEnvironmentService implements INativeEnvironmentService {
return undefined;
}
@memoize
get extensionDevelopmentKind(): ExtensionKind[] | undefined {
return this._args.extensionDevelopmentKind?.map(k => k === 'ui' || k === 'workspace' || k === 'web' ? k : 'workspace');
}
@memoize
get extensionTestsLocationURI(): URI | undefined {
const s = this._args.extensionTestsPath;

View file

@ -6,7 +6,7 @@
import { localize } from 'vs/nls';
import { mark } from 'vs/base/common/performance';
import { Disposable, IDisposable, toDisposable, dispose, DisposableStore } from 'vs/base/common/lifecycle';
import { IFileService, IResolveFileOptions, FileChangesEvent, FileOperationEvent, IFileSystemProviderRegistrationEvent, IFileSystemProvider, IFileStat, IResolveFileResult, ICreateFileOptions, IFileSystemProviderActivationEvent, FileOperationError, FileOperationResult, FileOperation, FileSystemProviderCapabilities, FileType, toFileSystemProviderErrorCode, FileSystemProviderErrorCode, IStat, IFileStatWithMetadata, IResolveMetadataFileOptions, etag, hasReadWriteCapability, hasFileFolderCopyCapability, hasOpenReadWriteCloseCapability, toFileOperationResult, IFileSystemProviderWithOpenReadWriteCloseCapability, IFileSystemProviderWithFileReadWriteCapability, IResolveFileResultWithMetadata, IWatchOptions, IWriteFileOptions, IReadFileOptions, IFileStreamContent, IFileContent, ETAG_DISABLED, hasFileReadStreamCapability, IFileSystemProviderWithFileReadStreamCapability, ensureFileSystemProviderError, IFileSystemProviderCapabilitiesChangeEvent, IReadFileStreamOptions } from 'vs/platform/files/common/files';
import { IFileService, IResolveFileOptions, FileChangesEvent, FileOperationEvent, IFileSystemProviderRegistrationEvent, IFileSystemProvider, IFileStat, IResolveFileResult, ICreateFileOptions, IFileSystemProviderActivationEvent, FileOperationError, FileOperationResult, FileOperation, FileSystemProviderCapabilities, FileType, toFileSystemProviderErrorCode, FileSystemProviderErrorCode, IStat, IFileStatWithMetadata, IResolveMetadataFileOptions, etag, hasReadWriteCapability, hasFileFolderCopyCapability, hasOpenReadWriteCloseCapability, toFileOperationResult, IFileSystemProviderWithOpenReadWriteCloseCapability, IFileSystemProviderWithFileReadWriteCapability, IResolveFileResultWithMetadata, IWatchOptions, IWriteFileOptions, IReadFileOptions, IFileStreamContent, IFileContent, ETAG_DISABLED, hasFileReadStreamCapability, IFileSystemProviderWithFileReadStreamCapability, ensureFileSystemProviderError, IFileSystemProviderCapabilitiesChangeEvent, IReadFileStreamOptions, FileDeleteOptions } from 'vs/platform/files/common/files';
import { URI } from 'vs/base/common/uri';
import { Emitter } from 'vs/base/common/event';
import { IExtUri, extUri, extUriIgnorePathCase, isAbsolutePath } from 'vs/base/common/resources';
@ -362,12 +362,12 @@ export class FileService extends Disposable implements IFileService {
// write file: unbuffered (only if data to write is a buffer, or the provider has no buffered write capability)
if (!hasOpenReadWriteCloseCapability(provider) || (hasReadWriteCapability(provider) && bufferOrReadableOrStreamOrBufferedStream instanceof VSBuffer)) {
await this.doWriteUnbuffered(provider, resource, bufferOrReadableOrStreamOrBufferedStream);
await this.doWriteUnbuffered(provider, resource, options, bufferOrReadableOrStreamOrBufferedStream);
}
// write file: buffered
else {
await this.doWriteBuffered(provider, resource, bufferOrReadableOrStreamOrBufferedStream instanceof VSBuffer ? bufferToReadable(bufferOrReadableOrStreamOrBufferedStream) : bufferOrReadableOrStreamOrBufferedStream);
await this.doWriteBuffered(provider, resource, options, bufferOrReadableOrStreamOrBufferedStream instanceof VSBuffer ? bufferToReadable(bufferOrReadableOrStreamOrBufferedStream) : bufferOrReadableOrStreamOrBufferedStream);
}
} catch (error) {
throw new FileOperationError(localize('err.write', "Unable to write file '{0}' ({1})", this.resourceForError(resource), ensureFileSystemProviderError(error).toString()), toFileOperationResult(error), options);
@ -377,6 +377,14 @@ export class FileService extends Disposable implements IFileService {
}
private async validateWriteFile(provider: IFileSystemProvider, resource: URI, options?: IWriteFileOptions): Promise<IStat | undefined> {
// Validate unlock support
const unlock = !!options?.unlock;
if (unlock && !(provider.capabilities & FileSystemProviderCapabilities.FileWriteUnlock)) {
throw new Error(localize('writeFailedUnlockUnsupported', "Unable to unlock file '{0}' because provider does not support it.", this.resourceForError(resource)));
}
// Validate via file stat meta data
let stat: IStat | undefined = undefined;
try {
stat = await provider.stat(resource);
@ -384,7 +392,7 @@ export class FileService extends Disposable implements IFileService {
return undefined; // file might not exist
}
// file cannot be directory
// File cannot be directory
if ((stat.type & FileType.Directory) !== 0) {
throw new FileOperationError(localize('fileIsDirectoryWriteError', "Unable to write file '{0}' that is actually a directory", this.resourceForError(resource)), FileOperationResult.FILE_IS_DIRECTORY, options);
}
@ -880,7 +888,7 @@ export class FileService extends Disposable implements IFileService {
}
}
async canDelete(resource: URI, options?: { useTrash?: boolean; recursive?: boolean; }): Promise<Error | true> {
async canDelete(resource: URI, options?: Partial<FileDeleteOptions>): Promise<Error | true> {
try {
await this.doValidateDelete(resource, options);
} catch (error) {
@ -890,7 +898,7 @@ export class FileService extends Disposable implements IFileService {
return true;
}
private async doValidateDelete(resource: URI, options?: { useTrash?: boolean; recursive?: boolean; }): Promise<IFileSystemProvider> {
private async doValidateDelete(resource: URI, options?: Partial<FileDeleteOptions>): Promise<IFileSystemProvider> {
const provider = this.throwIfFileSystemIsReadonly(await this.withProvider(resource), resource);
// Validate trash support
@ -917,7 +925,7 @@ export class FileService extends Disposable implements IFileService {
return provider;
}
async del(resource: URI, options?: { useTrash?: boolean; recursive?: boolean; }): Promise<void> {
async del(resource: URI, options?: Partial<FileDeleteOptions>): Promise<void> {
const provider = await this.doValidateDelete(resource, options);
const useTrash = !!options?.useTrash;
@ -1005,11 +1013,11 @@ export class FileService extends Disposable implements IFileService {
private readonly writeQueue = this._register(new ResourceQueue());
private async doWriteBuffered(provider: IFileSystemProviderWithOpenReadWriteCloseCapability, resource: URI, readableOrStreamOrBufferedStream: VSBufferReadable | VSBufferReadableStream | VSBufferReadableBufferedStream): Promise<void> {
private async doWriteBuffered(provider: IFileSystemProviderWithOpenReadWriteCloseCapability, resource: URI, options: IWriteFileOptions | undefined, readableOrStreamOrBufferedStream: VSBufferReadable | VSBufferReadableStream | VSBufferReadableBufferedStream): Promise<void> {
return this.writeQueue.queueFor(resource, this.getExtUri(provider).providerExtUri).queue(async () => {
// open handle
const handle = await provider.open(resource, { create: true });
const handle = await provider.open(resource, { create: true, unlock: options?.unlock ?? false });
// write into handle until all bytes from buffer have been written
try {
@ -1104,11 +1112,11 @@ export class FileService extends Disposable implements IFileService {
}
}
private async doWriteUnbuffered(provider: IFileSystemProviderWithFileReadWriteCapability, resource: URI, bufferOrReadableOrStreamOrBufferedStream: VSBuffer | VSBufferReadable | VSBufferReadableStream | VSBufferReadableBufferedStream): Promise<void> {
return this.writeQueue.queueFor(resource, this.getExtUri(provider).providerExtUri).queue(() => this.doWriteUnbufferedQueued(provider, resource, bufferOrReadableOrStreamOrBufferedStream));
private async doWriteUnbuffered(provider: IFileSystemProviderWithFileReadWriteCapability, resource: URI, options: IWriteFileOptions | undefined, bufferOrReadableOrStreamOrBufferedStream: VSBuffer | VSBufferReadable | VSBufferReadableStream | VSBufferReadableBufferedStream): Promise<void> {
return this.writeQueue.queueFor(resource, this.getExtUri(provider).providerExtUri).queue(() => this.doWriteUnbufferedQueued(provider, resource, options, bufferOrReadableOrStreamOrBufferedStream));
}
private async doWriteUnbufferedQueued(provider: IFileSystemProviderWithFileReadWriteCapability, resource: URI, bufferOrReadableOrStreamOrBufferedStream: VSBuffer | VSBufferReadable | VSBufferReadableStream | VSBufferReadableBufferedStream): Promise<void> {
private async doWriteUnbufferedQueued(provider: IFileSystemProviderWithFileReadWriteCapability, resource: URI, options: IWriteFileOptions | undefined, bufferOrReadableOrStreamOrBufferedStream: VSBuffer | VSBufferReadable | VSBufferReadableStream | VSBufferReadableBufferedStream): Promise<void> {
let buffer: VSBuffer;
if (bufferOrReadableOrStreamOrBufferedStream instanceof VSBuffer) {
buffer = bufferOrReadableOrStreamOrBufferedStream;
@ -1121,7 +1129,7 @@ export class FileService extends Disposable implements IFileService {
}
// Write through the provider
await provider.writeFile(resource, buffer.buffer, { create: true, overwrite: true });
await provider.writeFile(resource, buffer.buffer, { create: true, overwrite: true, unlock: options?.unlock ?? false });
}
private async doPipeBuffered(sourceProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, source: URI, targetProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, target: URI): Promise<void> {
@ -1136,7 +1144,7 @@ export class FileService extends Disposable implements IFileService {
// Open handles
sourceHandle = await sourceProvider.open(source, { create: false });
targetHandle = await targetProvider.open(target, { create: true });
targetHandle = await targetProvider.open(target, { create: true, unlock: false });
const buffer = VSBuffer.alloc(this.BUFFER_SIZE);
@ -1175,7 +1183,7 @@ export class FileService extends Disposable implements IFileService {
}
private async doPipeUnbufferedQueued(sourceProvider: IFileSystemProviderWithFileReadWriteCapability, source: URI, targetProvider: IFileSystemProviderWithFileReadWriteCapability, target: URI): Promise<void> {
return targetProvider.writeFile(target, await sourceProvider.readFile(source), { create: true, overwrite: true });
return targetProvider.writeFile(target, await sourceProvider.readFile(source), { create: true, overwrite: true, unlock: false });
}
private async doPipeUnbufferedToBuffered(sourceProvider: IFileSystemProviderWithFileReadWriteCapability, source: URI, targetProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, target: URI): Promise<void> {
@ -1185,7 +1193,7 @@ export class FileService extends Disposable implements IFileService {
private async doPipeUnbufferedToBufferedQueued(sourceProvider: IFileSystemProviderWithFileReadWriteCapability, source: URI, targetProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, target: URI): Promise<void> {
// Open handle
const targetHandle = await targetProvider.open(target, { create: true });
const targetHandle = await targetProvider.open(target, { create: true, unlock: false });
// Read entire buffer from source and write buffered
try {
@ -1204,7 +1212,7 @@ export class FileService extends Disposable implements IFileService {
const buffer = await streamToBuffer(this.readFileBuffered(sourceProvider, source, CancellationToken.None));
// Write buffer into target at once
await this.doWriteUnbuffered(targetProvider, target, buffer);
await this.doWriteUnbuffered(targetProvider, target, undefined, buffer);
}
protected throwIfFileSystemIsReadonly<T extends IFileSystemProvider>(provider: T, resource: URI): T {

View file

@ -17,6 +17,8 @@ import { ReadableStreamEvents } from 'vs/base/common/stream';
import { CancellationToken } from 'vs/base/common/cancellation';
import { TernarySearchTree } from 'vs/base/common/map';
//#region file service & providers
export const IFileService = createDecorator<IFileService>('fileService');
export interface IFileService {
@ -170,13 +172,13 @@ export interface IFileService {
* move the file to trash. The optional recursive parameter allows to delete
* non-empty folders recursively.
*/
del(resource: URI, options?: { useTrash?: boolean, recursive?: boolean }): Promise<void>;
del(resource: URI, options?: Partial<FileDeleteOptions>): Promise<void>;
/**
* Find out if a delete operation is possible given the arguments. No changes on disk will
* be performed. Returns an Error if the operation cannot be done.
*/
canDelete(resource: URI, options?: { useTrash?: boolean, recursive?: boolean }): Promise<Error | true>;
canDelete(resource: URI, options?: Partial<FileDeleteOptions>): Promise<Error | true>;
/**
* Allows to start a watcher that reports file/folder change events on the provided resource.
@ -192,9 +194,24 @@ export interface IFileService {
}
export interface FileOverwriteOptions {
/**
* Set to `true` to overwrite a file if it exists. Will
* throw an error otherwise if the file does exist.
*/
overwrite: boolean;
}
export interface FileUnlockOptions {
/**
* Set to `true` to try to remove any write locks the file might
* have. A file that is write locked will throw an error for any
* attempt to write to unless `unlock: true` is provided.
*/
unlock: boolean;
}
export interface FileReadStreamOptions {
/**
@ -218,28 +235,86 @@ export interface FileReadStreamOptions {
};
}
export interface FileWriteOptions {
overwrite: boolean;
export interface FileWriteOptions extends FileOverwriteOptions, FileUnlockOptions {
/**
* Set to `true` to create a file when it does not exist. Will
* throw an error otherwise if the file does not exist.
*/
create: boolean;
}
export interface FileOpenOptions {
create: boolean;
export type FileOpenOptions = FileOpenForReadOptions | FileOpenForWriteOptions;
export function isFileOpenForWriteOptions(options: FileOpenOptions): options is FileOpenForWriteOptions {
return options.create === true;
}
export interface FileOpenForReadOptions {
/**
* A hint that the file should be opened for reading only.
*/
create: false;
}
export interface FileOpenForWriteOptions extends FileUnlockOptions {
/**
* A hint that the file should be opened for reading and writing.
*/
create: true;
}
export interface FileDeleteOptions {
/**
* Set to `true` to recursively delete any children of the file. This
* only applies to folders and can lead to an error unless provided
* if the folder is not empty.
*/
recursive: boolean;
/**
* Set to `true` to attempt to move the file to trash
* instead of deleting it permanently from disk. This
* option maybe not be supported on all providers.
*/
useTrash: boolean;
}
export enum FileType {
/**
* File is unknown (neither file, directory nor symbolic link).
*/
Unknown = 0,
/**
* File is a normal file.
*/
File = 1,
/**
* File is a directory.
*/
Directory = 2,
/**
* File is a symbolic link.
*
* Note: even when the file is a symbolic link, you can test for
* `FileType.File` and `FileType.Directory` to know the type of
* the target the link points to.
*/
SymbolicLink = 64
}
export interface IStat {
/**
* The file type.
*/
type: FileType;
/**
@ -252,25 +327,67 @@ export interface IStat {
*/
ctime: number;
/**
* The size of the file in bytes.
*/
size: number;
}
export interface IWatchOptions {
/**
* Set to `true` to watch for changes recursively in a folder
* and all of its children.
*/
recursive: boolean;
/**
* A set of paths to exclude from watching.
*/
excludes: string[];
}
export const enum FileSystemProviderCapabilities {
/**
* Provider supports unbuffered read/write.
*/
FileReadWrite = 1 << 1,
/**
* Provider supports open/read/write/close low level file operations.
*/
FileOpenReadWriteClose = 1 << 2,
/**
* Provider supports stream based reading.
*/
FileReadStream = 1 << 4,
/**
* Provider supports copy operation.
*/
FileFolderCopy = 1 << 3,
/**
* Provider is path case sensitive.
*/
PathCaseSensitive = 1 << 10,
/**
* All files of the provider are readonly.
*/
Readonly = 1 << 11,
Trash = 1 << 12
/**
* Provider supports to delete via trash.
*/
Trash = 1 << 12,
/**
* Provider support to unlock files for writing.
*/
FileWriteUnlock = 1 << 13
}
export interface IFileSystemProvider {
@ -345,6 +462,7 @@ export enum FileSystemProviderErrorCode {
FileIsADirectory = 'EntryIsADirectory',
FileExceedsMemoryLimit = 'EntryExceedsMemoryLimit',
FileTooLarge = 'EntryTooLarge',
FileWriteLocked = 'EntryWriteLocked',
NoPermissions = 'NoPermissions',
Unavailable = 'Unavailable',
Unknown = 'Unknown'
@ -404,6 +522,7 @@ export function toFileSystemProviderErrorCode(error: Error | undefined | null):
case FileSystemProviderErrorCode.FileNotFound: return FileSystemProviderErrorCode.FileNotFound;
case FileSystemProviderErrorCode.FileExceedsMemoryLimit: return FileSystemProviderErrorCode.FileExceedsMemoryLimit;
case FileSystemProviderErrorCode.FileTooLarge: return FileSystemProviderErrorCode.FileTooLarge;
case FileSystemProviderErrorCode.FileWriteLocked: return FileSystemProviderErrorCode.FileWriteLocked;
case FileSystemProviderErrorCode.NoPermissions: return FileSystemProviderErrorCode.NoPermissions;
case FileSystemProviderErrorCode.Unavailable: return FileSystemProviderErrorCode.Unavailable;
}
@ -426,6 +545,8 @@ export function toFileOperationResult(error: Error): FileOperationResult {
return FileOperationResult.FILE_IS_DIRECTORY;
case FileSystemProviderErrorCode.FileNotADirectory:
return FileOperationResult.FILE_NOT_DIRECTORY;
case FileSystemProviderErrorCode.FileWriteLocked:
return FileOperationResult.FILE_WRITE_LOCKED;
case FileSystemProviderErrorCode.NoPermissions:
return FileOperationResult.FILE_PERMISSION_DENIED;
case FileSystemProviderErrorCode.FileExists:
@ -479,9 +600,9 @@ export class FileOperationEvent {
* Possible changes that can occur to a file.
*/
export const enum FileChangeType {
UPDATED = 0,
ADDED = 1,
DELETED = 2
UPDATED,
ADDED,
DELETED
}
/**
@ -744,12 +865,7 @@ interface IBaseStat {
etag?: string;
}
export interface IBaseStatWithMetadata extends IBaseStat {
mtime: number;
ctime: number;
etag: string;
size: number;
}
export interface IBaseStatWithMetadata extends Required<IBaseStat> { }
/**
* A file resource with meta information.
@ -767,7 +883,10 @@ export interface IFileStat extends IBaseStat {
isDirectory: boolean;
/**
* The resource is a symbolic link.
* The resource is a symbolic link. Note: even when the
* file is a symbolic link, you can test for `FileType.File`
* and `FileType.Directory` to know the type of the target
* the link points to.
*/
isSymbolicLink: boolean;
@ -854,6 +973,11 @@ export interface IWriteFileOptions {
* The etag of the file. This can be used to prevent dirty writes.
*/
readonly etag?: string;
/**
* Whether to attempt to unlock a file before writing.
*/
readonly unlock?: boolean;
}
export interface IResolveFileOptions {
@ -905,7 +1029,7 @@ export const enum FileOperationResult {
FILE_NOT_MODIFIED_SINCE,
FILE_MODIFIED_SINCE,
FILE_MOVE_CONFLICT,
FILE_READ_ONLY,
FILE_WRITE_LOCKED,
FILE_PERMISSION_DENIED,
FILE_TOO_LARGE,
FILE_INVALID_PATH,
@ -914,6 +1038,10 @@ export const enum FileOperationResult {
FILE_OTHER_ERROR
}
//#endregion
//#region Settings
export const AutoSaveConfiguration = {
OFF: 'off',
AFTER_DELAY: 'afterDelay',
@ -948,6 +1076,10 @@ export interface IFilesConfiguration {
};
}
//#endregion
//#region Utilities
export enum FileKind {
FILE,
FOLDER,
@ -1042,3 +1174,5 @@ export function getPlatformLimits(arch: Arch): IArchLimits {
maxHeapSize: arch === Arch.IA32 ? 700 * ByteSize.MB : 2 * 700 * ByteSize.MB, // https://github.com/v8/v8/blob/5918a23a3d571b9625e5cce246bdd5b46ff7cd8b/src/heap/heap.cc#L149
};
}
//#endregion

View file

@ -6,7 +6,7 @@
import { open, close, read, write, fdatasync, Stats, promises } from 'fs';
import { promisify } from 'util';
import { IDisposable, Disposable, toDisposable, dispose, combinedDisposable } from 'vs/base/common/lifecycle';
import { FileSystemProviderCapabilities, IFileChange, IWatchOptions, IStat, FileType, FileDeleteOptions, FileOverwriteOptions, FileWriteOptions, FileOpenOptions, FileSystemProviderErrorCode, createFileSystemProviderError, FileSystemProviderError, IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithFileReadStreamCapability, IFileSystemProviderWithOpenReadWriteCloseCapability, FileReadStreamOptions, IFileSystemProviderWithFileFolderCopyCapability } from 'vs/platform/files/common/files';
import { FileSystemProviderCapabilities, IFileChange, IWatchOptions, IStat, FileType, FileDeleteOptions, FileOverwriteOptions, FileWriteOptions, FileOpenOptions, FileSystemProviderErrorCode, createFileSystemProviderError, FileSystemProviderError, IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithFileReadStreamCapability, IFileSystemProviderWithOpenReadWriteCloseCapability, FileReadStreamOptions, IFileSystemProviderWithFileFolderCopyCapability, isFileOpenForWriteOptions } from 'vs/platform/files/common/files';
import { URI } from 'vs/base/common/uri';
import { Event, Emitter } from 'vs/base/common/event';
import { isLinux, isWindows } from 'vs/base/common/platform';
@ -64,7 +64,8 @@ export class DiskFileSystemProvider extends Disposable implements
FileSystemProviderCapabilities.FileReadWrite |
FileSystemProviderCapabilities.FileOpenReadWriteClose |
FileSystemProviderCapabilities.FileReadStream |
FileSystemProviderCapabilities.FileFolderCopy;
FileSystemProviderCapabilities.FileFolderCopy |
FileSystemProviderCapabilities.FileWriteUnlock;
if (isLinux) {
this._capabilities |= FileSystemProviderCapabilities.PathCaseSensitive;
@ -188,12 +189,12 @@ export class DiskFileSystemProvider extends Disposable implements
}
// Open
handle = await this.open(resource, { create: true });
handle = await this.open(resource, { create: true, unlock: opts.unlock });
// Write content at once
await this.write(handle, 0, content, 0, content.byteLength);
} catch (error) {
throw this.toFileSystemProviderError(error);
throw await this.toFileSystemProviderWriteError(resource, error);
} finally {
if (typeof handle === 'number') {
await this.close(handle);
@ -203,15 +204,28 @@ export class DiskFileSystemProvider extends Disposable implements
private readonly mapHandleToPos: Map<number, number> = new Map();
private readonly writeHandles: Set<number> = new Set();
private readonly writeHandles = new Map<number, URI>();
private canFlush: boolean = true;
async open(resource: URI, opts: FileOpenOptions): Promise<number> {
try {
const filePath = this.toFilePath(resource);
// Determine wether to unlock the file (write only)
if (isFileOpenForWriteOptions(opts) && opts.unlock) {
try {
const { stat } = await SymlinkSupport.stat(filePath);
if (!(stat.mode & 0o200 /* File mode indicating writable by owner */)) {
await promises.chmod(filePath, stat.mode | 0o200);
}
} catch (error) {
this.logService.trace(error); // ignore any errors here and try to just write
}
}
// Determine file flags for opening (read vs write)
let flags: string | undefined = undefined;
if (opts.create) {
if (isFileOpenForWriteOptions(opts)) {
if (isWindows) {
try {
// On Windows and if the file exists, we use a different strategy of saving the file
@ -252,13 +266,17 @@ export class DiskFileSystemProvider extends Disposable implements
this.mapHandleToPos.set(handle, 0);
// remember that this handle was used for writing
if (opts.create) {
this.writeHandles.add(handle);
if (isFileOpenForWriteOptions(opts)) {
this.writeHandles.set(handle, resource);
}
return handle;
} catch (error) {
throw this.toFileSystemProviderError(error);
if (isFileOpenForWriteOptions(opts)) {
throw await this.toFileSystemProviderWriteError(resource, error);
} else {
throw this.toFileSystemProviderError(error);
}
}
}
@ -388,7 +406,7 @@ export class DiskFileSystemProvider extends Disposable implements
return bytesWritten;
} catch (error) {
throw this.toFileSystemProviderError(error);
throw await this.toFileSystemProviderWriteError(this.writeHandles.get(fd), error);
} finally {
this.updatePos(fd, normalizedPos, bytesWritten);
}
@ -690,6 +708,26 @@ export class DiskFileSystemProvider extends Disposable implements
return createFileSystemProviderError(error, code);
}
private async toFileSystemProviderWriteError(resource: URI | undefined, error: NodeJS.ErrnoException): Promise<FileSystemProviderError> {
let fileSystemProviderWriteError = this.toFileSystemProviderError(error);
// If the write error signals permission issues, we try
// to read the file's mode to see if the file is write
// locked.
if (resource && fileSystemProviderWriteError.code === FileSystemProviderErrorCode.NoPermissions) {
try {
const { stat } = await SymlinkSupport.stat(this.toFilePath(resource));
if (!(stat.mode & 0o200 /* File mode indicating writable by owner */)) {
fileSystemProviderWriteError = createFileSystemProviderError(error, FileSystemProviderErrorCode.FileWriteLocked);
}
} catch (error) {
this.logService.trace(error); // ignore - return original error
}
}
return fileSystemProviderWriteError;
}
//#endregion
dispose(): void {

View file

@ -66,6 +66,7 @@ export class TestDiskFileSystemProvider extends DiskFileSystemProvider {
FileSystemProviderCapabilities.FileOpenReadWriteClose |
FileSystemProviderCapabilities.FileReadStream |
FileSystemProviderCapabilities.Trash |
FileSystemProviderCapabilities.FileWriteUnlock |
FileSystemProviderCapabilities.FileFolderCopy;
if (isLinux) {
@ -1899,6 +1900,63 @@ flakySuite('Disk File Service', function () {
assert.strictEqual(readFileSync(resource.fsPath).toString(), content);
});
test('writeFile - locked files and unlocking', async () => {
setCapabilities(fileProvider, FileSystemProviderCapabilities.FileReadWrite | FileSystemProviderCapabilities.FileWriteUnlock);
return testLockedFiles(false);
});
test('writeFile (stream) - locked files and unlocking', async () => {
setCapabilities(fileProvider, FileSystemProviderCapabilities.FileOpenReadWriteClose | FileSystemProviderCapabilities.FileWriteUnlock);
return testLockedFiles(false);
});
test('writeFile - locked files and unlocking throws error when missing capability', async () => {
setCapabilities(fileProvider, FileSystemProviderCapabilities.FileReadWrite);
return testLockedFiles(true);
});
test('writeFile (stream) - locked files and unlocking throws error when missing capability', async () => {
setCapabilities(fileProvider, FileSystemProviderCapabilities.FileOpenReadWriteClose);
return testLockedFiles(true);
});
async function testLockedFiles(expectError: boolean) {
const lockedFile = URI.file(join(testDir, 'my-locked-file'));
await service.writeFile(lockedFile, VSBuffer.fromString('Locked File'));
const stats = await promises.stat(lockedFile.fsPath);
await promises.chmod(lockedFile.fsPath, stats.mode & ~0o200);
let error;
const newContent = 'Updates to locked file';
try {
await service.writeFile(lockedFile, VSBuffer.fromString(newContent));
} catch (e) {
error = e;
}
assert.ok(error);
error = undefined;
if (expectError) {
try {
await service.writeFile(lockedFile, VSBuffer.fromString(newContent), { unlock: true });
} catch (e) {
error = e;
}
assert.ok(error);
} else {
await service.writeFile(lockedFile, VSBuffer.fromString(newContent), { unlock: true });
assert.strictEqual(readFileSync(lockedFile.fsPath).toString(), newContent);
}
}
test('writeFile (error when folder is encountered)', async () => {
const resource = URI.file(testDir);
@ -2296,7 +2354,7 @@ flakySuite('Disk File Service', function () {
const resource = URI.file(join(testDir, 'lorem.txt'));
const buffer = VSBuffer.alloc(1024);
const fdWrite = await fileProvider.open(resource, { create: true });
const fdWrite = await fileProvider.open(resource, { create: true, unlock: false });
const fdRead = await fileProvider.open(resource, { create: false });
let posInFileWrite = 0;

View file

@ -578,10 +578,6 @@ export interface IOpenResourceOptions {
payload: any;
}
export interface IResourceResultsNavigationOptions {
openOnFocus: boolean;
}
export interface IOpenEvent<T> {
editorOptions: IEditorOptions;
sideBySide: boolean;
@ -591,7 +587,6 @@ export interface IOpenEvent<T> {
export interface IResourceNavigatorOptions {
readonly configurationService?: IConfigurationService;
readonly openOnFocus?: boolean;
readonly openOnSingleClick?: boolean;
}
@ -612,7 +607,6 @@ export function getSelectionKeyboardEvent(typeArg = 'keydown', preserveFocus?: b
abstract class ResourceNavigator<T> extends Disposable {
private readonly openOnFocus: boolean;
private openOnSingleClick: boolean;
private readonly _onDidOpen = this._register(new Emitter<IOpenEvent<T | undefined>>());
@ -624,16 +618,10 @@ abstract class ResourceNavigator<T> extends Disposable {
) {
super();
this.openOnFocus = options?.openOnFocus ?? false;
this._register(Event.filter(this.widget.onDidChangeSelection, e => e.browserEvent instanceof KeyboardEvent)(e => this.onSelectionFromKeyboard(e)));
this._register(this.widget.onPointer((e: { browserEvent: MouseEvent, element: T | undefined }) => this.onPointer(e.element, e.browserEvent)));
this._register(this.widget.onMouseDblClick((e: { browserEvent: MouseEvent, element: T | undefined }) => this.onMouseDblClick(e.element, e.browserEvent)));
if (this.openOnFocus) {
this._register(Event.filter(this.widget.onDidChangeFocus, e => e.browserEvent instanceof KeyboardEvent)(e => this.onFocusFromKeyboard(e)));
}
if (typeof options?.openOnSingleClick !== 'boolean' && options?.configurationService) {
this.openOnSingleClick = options?.configurationService!.getValue(openModeSettingKey) !== 'doubleClick';
this._register(options?.configurationService.onDidChangeConfiguration(() => {
@ -644,18 +632,6 @@ abstract class ResourceNavigator<T> extends Disposable {
}
}
private onFocusFromKeyboard(event: ITreeEvent<any>): void {
const focus = this.widget.getFocus();
this.widget.setSelection(focus, event.browserEvent);
const selectionKeyboardEvent = event.browserEvent as SelectionKeyboardEvent;
const preserveFocus = typeof selectionKeyboardEvent.preserveFocus === 'boolean' ? selectionKeyboardEvent.preserveFocus! : true;
const pinned = typeof selectionKeyboardEvent.pinned === 'boolean' ? selectionKeyboardEvent.pinned! : !preserveFocus;
const sideBySide = false;
this._open(this.getSelectedElement(), preserveFocus, pinned, sideBySide, event.browserEvent);
}
private onSelectionFromKeyboard(event: ITreeEvent<any>): void {
if (event.elements.length !== 1) {
return;

View file

@ -100,7 +100,7 @@ export interface ICommonNativeHostService {
moveItemToTrash(fullPath: string, deleteOnFail?: boolean): Promise<boolean>;
isAdmin(): Promise<boolean>;
writeElevated(source: URI, target: URI, options?: { overwriteReadonly?: boolean }): Promise<void>;
writeElevated(source: URI, target: URI, options?: { unlock?: boolean }): Promise<void>;
getOSProperties(): Promise<IOSProperties>;
getOSStatistics(): Promise<IOSStatistics>;

View file

@ -390,12 +390,12 @@ export class NativeHostMainService extends Disposable implements INativeHostMain
return isAdmin;
}
async writeElevated(windowId: number | undefined, source: URI, target: URI, options?: { overwriteReadonly?: boolean }): Promise<void> {
async writeElevated(windowId: number | undefined, source: URI, target: URI, options?: { unlock?: boolean }): Promise<void> {
const sudoPrompt = await import('sudo-prompt');
return new Promise<void>((resolve, reject) => {
const sudoCommand: string[] = [`"${this.cliPath}"`];
if (options?.overwriteReadonly) {
if (options?.unlock) {
sudoCommand.push('--file-chmod');
}

View file

@ -923,7 +923,6 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic
return undefined;
}
private doResolvePathRemote(path: string, remoteAuthority: string, forceOpenWorkspaceAsFile?: boolean): IPathToOpen | undefined {
const first = path.charCodeAt(0);

View file

@ -122,10 +122,6 @@ export class MainThreadExtensionService implements MainThreadExtensionServiceSha
}
}
async $onExtensionHostExit(code: number): Promise<void> {
this._extensionService._onExtensionHostExit(code);
}
async $setPerformanceMarks(marks: PerformanceMark[]): Promise<void> {
if (this._extensionHostKind === ExtensionHostKind.LocalProcess) {
this._timerService.setPerformanceMarks('localExtHost', marks);

View file

@ -938,7 +938,6 @@ export interface MainThreadExtensionServiceShape extends IDisposable {
$onDidActivateExtension(extensionId: ExtensionIdentifier, codeLoadingTime: number, activateCallTime: number, activateResolvedTime: number, activationReason: ExtensionActivationReason): void;
$onExtensionActivationError(extensionId: ExtensionIdentifier, error: ExtensionActivationError): Promise<void>;
$onExtensionRuntimeError(extensionId: ExtensionIdentifier, error: SerializedError): void;
$onExtensionHostExit(code: number): Promise<void>;
$setPerformanceMarks(marks: performance.PerformanceMark[]): Promise<void>;
}
@ -1228,6 +1227,8 @@ export type IResolveAuthorityResult = IResolveAuthorityErrorResult | IResolveAut
export interface ExtHostExtensionServiceShape {
$resolveAuthority(remoteAuthority: string, resolveAttempt: number): Promise<IResolveAuthorityResult>;
$startExtensionHost(enabledExtensionIds: ExtensionIdentifier[]): Promise<void>;
$extensionTestsExecute(): Promise<number>;
$extensionTestsExit(code: number): Promise<void>;
$activateByEvent(activationEvent: string, activationKind: ActivationKind): Promise<void>;
$activate(extensionId: ExtensionIdentifier, reason: ExtensionActivationReason): Promise<boolean>;
$setRemoteEnvironment(env: { [key: string]: string | null; }): Promise<void>;

View file

@ -547,7 +547,7 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme
);
}
private _handleExtensionTests(): Promise<void> {
public $extensionTestsExecute(): Promise<number> {
return this._doHandleExtensionTests().then(undefined, error => {
console.error(error); // ensure any error message makes it onto the console
@ -555,76 +555,51 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme
});
}
private async _doHandleExtensionTests(): Promise<void> {
private async _doHandleExtensionTests(): Promise<number> {
const { extensionDevelopmentLocationURI, extensionTestsLocationURI } = this._initData.environment;
if (!(extensionDevelopmentLocationURI && extensionTestsLocationURI && extensionTestsLocationURI.scheme === Schemas.file)) {
return Promise.resolve(undefined);
if (!extensionDevelopmentLocationURI || !extensionTestsLocationURI || extensionTestsLocationURI.scheme !== Schemas.file) {
throw new Error(nls.localize('extensionTestError1', "Cannot load test runner."));
}
const extensionTestsPath = originalFSPath(extensionTestsLocationURI);
// Require the test runner via node require from the provided path
let testRunner: ITestRunner | INewTestRunner | undefined;
let requireError: Error | undefined;
try {
testRunner = await this._loadCommonJSModule(null, URI.file(extensionTestsPath), new ExtensionActivationTimesBuilder(false));
} catch (error) {
requireError = error;
const testRunner: ITestRunner | INewTestRunner | undefined = await this._loadCommonJSModule(null, URI.file(extensionTestsPath), new ExtensionActivationTimesBuilder(false));
if (!testRunner || typeof testRunner.run !== 'function') {
throw new Error(nls.localize('extensionTestError', "Path {0} does not point to a valid extension test runner.", extensionTestsPath));
}
// Execute the runner if it follows the old `run` spec
if (testRunner && typeof testRunner.run === 'function') {
return new Promise<void>((c, e) => {
const oldTestRunnerCallback = (error: Error, failures: number | undefined) => {
if (error) {
e(error.toString());
} else {
c(undefined);
}
// after tests have run, we shutdown the host
this._testRunnerExit(error || (typeof failures === 'number' && failures > 0) ? 1 /* ERROR */ : 0 /* OK */);
};
const runResult = testRunner!.run(extensionTestsPath, oldTestRunnerCallback);
// Using the new API `run(): Promise<void>`
if (runResult && runResult.then) {
runResult
.then(() => {
c();
this._testRunnerExit(0);
})
.catch((err: Error) => {
e(err.toString());
this._testRunnerExit(1);
});
return new Promise<number>((resolve, reject) => {
const oldTestRunnerCallback = (error: Error, failures: number | undefined) => {
if (error) {
reject(error);
} else {
resolve((typeof failures === 'number' && failures > 0) ? 1 /* ERROR */ : 0 /* OK */);
}
});
}
};
// Otherwise make sure to shutdown anyway even in case of an error
else {
this._testRunnerExit(1 /* ERROR */);
}
const runResult = testRunner.run(extensionTestsPath, oldTestRunnerCallback);
return Promise.reject(new Error(requireError ? requireError.toString() : nls.localize('extensionTestError', "Path {0} does not point to a valid extension test runner.", extensionTestsPath)));
// Using the new API `run(): Promise<void>`
if (runResult && runResult.then) {
runResult
.then(() => {
resolve(0);
})
.catch((err: Error) => {
reject(err.toString());
});
}
});
}
private _testRunnerExit(code: number): void {
public async $extensionTestsExit(code: number): Promise<void> {
this._logService.info(`extension host terminating: test runner requested exit with code ${code}`);
this._logService.info(`exiting with code ${code}`);
this._logService.flush();
// wait at most 5000ms for the renderer to confirm our exit request and for the renderer socket to drain
// (this is to ensure all outstanding messages reach the renderer)
const exitPromise = this._mainThreadExtensionsProxy.$onExtensionHostExit(code);
const drainPromise = this._extHostContext.drain();
Promise.race([Promise.all([exitPromise, drainPromise]), timeout(5000)]).then(() => {
this._logService.info(`exiting with code ${code}`);
this._logService.flush();
this._hostUtils.exit(code);
});
this._hostUtils.exit(code);
}
private _startExtensionHost(): Promise<void> {
@ -636,7 +611,6 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme
return this._readyToStartExtensionHost.wait()
.then(() => this._readyToRunExtensions.open())
.then(() => this._handleEagerExtensions())
.then(() => this._handleExtensionTests())
.then(() => {
this._logService.info(`eager extensions activated`);
});

View file

@ -16,6 +16,7 @@ import { Schemas } from 'vs/base/common/network';
import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions';
import { ExtensionRuntime } from 'vs/workbench/api/common/extHostTypes';
import { CLIServer } from 'vs/workbench/api/node/extHostCLIServer';
import { realpathSync } from 'vs/base/node/extpath';
class NodeModuleRequireInterceptor extends RequireInterceptor {
@ -36,7 +37,7 @@ class NodeModuleRequireInterceptor extends RequireInterceptor {
}
return that._factories.get(request)!.load(
request,
URI.file(parent.filename),
URI.file(realpathSync(parent.filename)),
request => original.apply(this, [request, parent, isMain])
);
};

View file

@ -40,10 +40,7 @@ async function updateFocus(widget: WorkbenchListWidget, updateFocusFn: (widget:
const newFocus = widget.getFocus();
if (
(selection.length !== 1 || !equals(focus, selection) || equals(focus, newFocus))
&& !(focus.length === 0 && selection.length === 0)
) {
if (selection.length > 1 || !equals(focus, selection) || equals(focus, newFocus)) {
return;
}

View file

@ -10,7 +10,7 @@ import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/la
import { IHistoryService } from 'vs/workbench/services/history/common/history';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { CLOSE_EDITOR_COMMAND_ID, MOVE_ACTIVE_EDITOR_COMMAND_ID, ActiveEditorMoveArguments, SPLIT_EDITOR_LEFT, SPLIT_EDITOR_RIGHT, SPLIT_EDITOR_UP, SPLIT_EDITOR_DOWN, splitEditor, LAYOUT_EDITOR_GROUPS_COMMAND_ID, mergeAllGroups, UNPIN_EDITOR_COMMAND_ID } from 'vs/workbench/browser/parts/editor/editorCommands';
import { CLOSE_EDITOR_COMMAND_ID, MOVE_ACTIVE_EDITOR_COMMAND_ID, ActiveEditorMoveArguments, SPLIT_EDITOR_LEFT, SPLIT_EDITOR_RIGHT, SPLIT_EDITOR_UP, SPLIT_EDITOR_DOWN, splitEditor, LAYOUT_EDITOR_GROUPS_COMMAND_ID, UNPIN_EDITOR_COMMAND_ID } from 'vs/workbench/browser/parts/editor/editorCommands';
import { IEditorGroupsService, IEditorGroup, GroupsArrangement, GroupLocation, GroupDirection, preferredSideBySideGroupDirection, IFindGroupScope, GroupOrientation, EditorGroupLayout, GroupsOrder, OpenEditorContext } from 'vs/workbench/services/editor/common/editorGroupsService';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
@ -216,7 +216,7 @@ export class JoinAllGroupsAction extends Action {
}
async run(): Promise<void> {
mergeAllGroups(this.editorGroupService);
this.editorGroupService.mergeAllGroups();
}
}

View file

@ -279,17 +279,6 @@ function registerEditorGroupsLayoutCommand(): void {
});
}
export function mergeAllGroups(editorGroupService: IEditorGroupsService): void {
const target = editorGroupService.activeGroup;
for (const group of editorGroupService.getGroups(GroupsOrder.MOST_RECENTLY_ACTIVE)) {
if (group === target) {
continue; // keep target
}
editorGroupService.mergeGroup(group, target);
}
}
function registerDiffEditorCommands(): void {
KeybindingsRegistry.registerCommandAndKeybindingRule({
id: GOTO_NEXT_CHANGE,

View file

@ -764,6 +764,18 @@ export class EditorPart extends Part implements IEditorGroupsService, IEditorGro
return targetView;
}
mergeAllGroups(target = this.activeGroup): IEditorGroupView {
for (const group of this.getGroups(GroupsOrder.MOST_RECENTLY_ACTIVE)) {
if (group === target) {
continue; // keep target
}
this.mergeGroup(group, target);
}
return target;
}
private assertGroupView(group: IEditorGroupView | GroupIdentifier): IEditorGroupView {
let groupView: IEditorGroupView | undefined;
if (typeof group === 'number') {

View file

@ -89,7 +89,7 @@ import { isStandalone } from 'vs/base/browser/browser';
'workbench.editor.untitled.hint': {
'type': 'string',
'enum': ['text', 'button', 'hidden'],
'default': 'button',
'default': 'hidden',
'markdownDescription': localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'untitledHint' }, "Controls if the untitled hint should be inline text in the editor or a floating button or hidden.")
},
'workbench.editor.tabCloseButton': {

View file

@ -137,7 +137,7 @@ export class BulkEditPane extends ViewPane {
multipleSelectionSupport: false,
keyboardNavigationLabelProvider: new BulkEditNaviLabelProvider(),
sorter: new BulkEditSorter(),
openOnFocus: true
selectionNavigation: true
}
);

View file

@ -167,7 +167,7 @@ export class CommentsPanel extends ViewPane {
this.treeLabels = this._register(this.instantiationService.createInstance(ResourceLabels, this));
this.tree = this._register(this.instantiationService.createInstance(CommentsList, this.treeLabels, this.treeContainer, {
overrideStyles: { listBackground: this.getBackgroundColor() },
openOnFocus: true,
selectionNavigation: true,
accessibilityProvider: {
getAriaLabel(element: any): string {
if (element instanceof CommentsModel) {

View file

@ -3,13 +3,13 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vs/nls';
import { localize } from 'vs/nls';
import { toErrorMessage } from 'vs/base/common/errorMessage';
import { basename, isEqual } from 'vs/base/common/resources';
import { Action, IAction } from 'vs/base/common/actions';
import { URI } from 'vs/base/common/uri';
import { FileOperationError, FileOperationResult } from 'vs/platform/files/common/files';
import { ITextFileService, ISaveErrorHandler, ITextFileEditorModel, IResolvedTextFileEditorModel, IWriteTextFileOptions } from 'vs/workbench/services/textfile/common/textfiles';
import { ITextFileService, ISaveErrorHandler, ITextFileEditorModel, IResolvedTextFileEditorModel } from 'vs/workbench/services/textfile/common/textfiles';
import { ServicesAccessor, IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IDisposable, dispose, Disposable } from 'vs/base/common/lifecycle';
import { IWorkbenchContribution } from 'vs/workbench/common/contributions';
@ -38,7 +38,7 @@ export const CONFLICT_RESOLUTION_SCHEME = 'conflictResolution';
const LEARN_MORE_DIRTY_WRITE_IGNORE_KEY = 'learnMoreDirtyWriteError';
const conflictEditorHelp = nls.localize('userGuide', "Use the actions in the editor tool bar to either undo your changes or overwrite the content of the file with your changes.");
const conflictEditorHelp = localize('userGuide', "Use the actions in the editor tool bar to either undo your changes or overwrite the content of the file with your changes.");
// A handler for text file save error happening with conflict resolution actions
export class TextFileSaveErrorHandler extends Disposable implements ISaveErrorHandler, IWorkbenchContribution {
@ -123,7 +123,7 @@ export class TextFileSaveErrorHandler extends Disposable implements ISaveErrorHa
// Otherwise show the message that will lead the user into the save conflict editor.
else {
message = nls.localize('staleSaveError', "Failed to save '{0}': The content of the file is newer. Please compare your version with the file contents or overwrite the content of the file with your changes.", basename(resource));
message = localize('staleSaveError', "Failed to save '{0}': The content of the file is newer. Please compare your version with the file contents or overwrite the content of the file with your changes.", basename(resource));
primaryActions.push(this.instantiationService.createInstance(ResolveSaveConflictAction, model));
primaryActions.push(this.instantiationService.createInstance(SaveModelIgnoreModifiedSinceAction, model));
@ -134,19 +134,19 @@ export class TextFileSaveErrorHandler extends Disposable implements ISaveErrorHa
// Any other save error
else {
const isReadonly = fileOperationError.fileOperationResult === FileOperationResult.FILE_READ_ONLY;
const triedToMakeWriteable = isReadonly && fileOperationError.options && (fileOperationError.options as IWriteTextFileOptions).overwriteReadonly;
const isWriteLocked = fileOperationError.fileOperationResult === FileOperationResult.FILE_WRITE_LOCKED;
const triedToUnlock = isWriteLocked && fileOperationError.options?.unlock;
const isPermissionDenied = fileOperationError.fileOperationResult === FileOperationResult.FILE_PERMISSION_DENIED;
const canHandlePermissionOrReadonlyErrors = resource.scheme === Schemas.file; // https://github.com/microsoft/vscode/issues/48659
const canSaveElevated = resource.scheme === Schemas.file; // https://github.com/microsoft/vscode/issues/48659 TODO
// Save Elevated
if (canHandlePermissionOrReadonlyErrors && (isPermissionDenied || triedToMakeWriteable)) {
primaryActions.push(this.instantiationService.createInstance(SaveModelElevatedAction, model, !!triedToMakeWriteable));
if (canSaveElevated && (isPermissionDenied || triedToUnlock)) {
primaryActions.push(this.instantiationService.createInstance(SaveModelElevatedAction, model, !!triedToUnlock));
}
// Overwrite
else if (canHandlePermissionOrReadonlyErrors && isReadonly) {
primaryActions.push(this.instantiationService.createInstance(OverwriteReadonlyModelAction, model));
// Unlock
else if (isWriteLocked) {
primaryActions.push(this.instantiationService.createInstance(UnlockModelAction, model));
}
// Retry
@ -161,16 +161,16 @@ export class TextFileSaveErrorHandler extends Disposable implements ISaveErrorHa
primaryActions.push(this.instantiationService.createInstance(DiscardModelAction, model));
// Message
if (canHandlePermissionOrReadonlyErrors && isReadonly) {
if (triedToMakeWriteable) {
message = isWindows ? nls.localize('readonlySaveErrorAdmin', "Failed to save '{0}': File is read-only. Select 'Overwrite as Admin' to retry as administrator.", basename(resource)) : nls.localize('readonlySaveErrorSudo', "Failed to save '{0}': File is read-only. Select 'Overwrite as Sudo' to retry as superuser.", basename(resource));
if (isWriteLocked) {
if (triedToUnlock && canSaveElevated) {
message = isWindows ? localize('readonlySaveErrorAdmin', "Failed to save '{0}': File is read-only. Select 'Overwrite as Admin' to retry as administrator.", basename(resource)) : localize('readonlySaveErrorSudo', "Failed to save '{0}': File is read-only. Select 'Overwrite as Sudo' to retry as superuser.", basename(resource));
} else {
message = nls.localize('readonlySaveError', "Failed to save '{0}': File is read-only. Select 'Overwrite' to attempt to make it writeable.", basename(resource));
message = localize('readonlySaveError', "Failed to save '{0}': File is read-only. Select 'Overwrite' to attempt to make it writeable.", basename(resource));
}
} else if (canHandlePermissionOrReadonlyErrors && isPermissionDenied) {
message = isWindows ? nls.localize('permissionDeniedSaveError', "Failed to save '{0}': Insufficient permissions. Select 'Retry as Admin' to retry as administrator.", basename(resource)) : nls.localize('permissionDeniedSaveErrorSudo', "Failed to save '{0}': Insufficient permissions. Select 'Retry as Sudo' to retry as superuser.", basename(resource));
} else if (canSaveElevated && isPermissionDenied) {
message = isWindows ? localize('permissionDeniedSaveError', "Failed to save '{0}': Insufficient permissions. Select 'Retry as Admin' to retry as administrator.", basename(resource)) : localize('permissionDeniedSaveErrorSudo', "Failed to save '{0}': Insufficient permissions. Select 'Retry as Sudo' to retry as superuser.", basename(resource));
} else {
message = nls.localize({ key: 'genericSaveError', comment: ['{0} is the resource that failed to save and {1} the error message'] }, "Failed to save '{0}': {1}", basename(resource), toErrorMessage(error, false));
message = localize({ key: 'genericSaveError', comment: ['{0} is the resource that failed to save and {1} the error message'] }, "Failed to save '{0}': {1}", basename(resource), toErrorMessage(error, false));
}
}
@ -203,7 +203,7 @@ class ResolveConflictLearnMoreAction extends Action {
constructor(
@IOpenerService private readonly openerService: IOpenerService
) {
super('workbench.files.action.resolveConflictLearnMore', nls.localize('learnMore', "Learn More"));
super('workbench.files.action.resolveConflictLearnMore', localize('learnMore', "Learn More"));
}
async run(): Promise<void> {
@ -216,7 +216,7 @@ class DoNotShowResolveConflictLearnMoreAction extends Action {
constructor(
@IStorageService private readonly storageService: IStorageService
) {
super('workbench.files.action.resolveConflictLearnMoreDoNotShowAgain', nls.localize('dontShowAgain', "Don't Show Again"));
super('workbench.files.action.resolveConflictLearnMoreDoNotShowAgain', localize('dontShowAgain', "Don't Show Again"));
}
async run(notification: IDisposable): Promise<void> {
@ -236,14 +236,14 @@ class ResolveSaveConflictAction extends Action {
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IProductService private readonly productService: IProductService
) {
super('workbench.files.action.resolveConflict', nls.localize('compareChanges', "Compare"));
super('workbench.files.action.resolveConflict', localize('compareChanges', "Compare"));
}
async run(): Promise<void> {
if (!this.model.isDisposed()) {
const resource = this.model.resource;
const name = basename(resource);
const editorLabel = nls.localize('saveConflictDiffLabel', "{0} (in file) ↔ {1} (in {2}) - Resolve save conflict", name, name, this.productService.nameLong);
const editorLabel = localize('saveConflictDiffLabel', "{0} (in file) ↔ {1} (in {2}) - Resolve save conflict", name, name, this.productService.nameLong);
await TextFileContentProvider.open(resource, CONFLICT_RESOLUTION_SCHEME, editorLabel, this.editorService, { pinned: true });
@ -265,16 +265,16 @@ class SaveModelElevatedAction extends Action {
constructor(
private model: ITextFileEditorModel,
private triedToMakeWriteable: boolean
private triedToUnlock: boolean
) {
super('workbench.files.action.saveModelElevated', triedToMakeWriteable ? isWindows ? nls.localize('overwriteElevated', "Overwrite as Admin...") : nls.localize('overwriteElevatedSudo', "Overwrite as Sudo...") : isWindows ? nls.localize('saveElevated', "Retry as Admin...") : nls.localize('saveElevatedSudo', "Retry as Sudo..."));
super('workbench.files.action.saveModelElevated', triedToUnlock ? isWindows ? localize('overwriteElevated', "Overwrite as Admin...") : localize('overwriteElevatedSudo', "Overwrite as Sudo...") : isWindows ? localize('saveElevated', "Retry as Admin...") : localize('saveElevatedSudo', "Retry as Sudo..."));
}
async run(): Promise<void> {
if (!this.model.isDisposed()) {
await this.model.save({
writeElevated: true,
overwriteReadonly: this.triedToMakeWriteable,
writeUnlock: this.triedToUnlock,
reason: SaveReason.EXPLICIT
});
}
@ -286,7 +286,7 @@ class RetrySaveModelAction extends Action {
constructor(
private model: ITextFileEditorModel
) {
super('workbench.files.action.saveModel', nls.localize('retry', "Retry"));
super('workbench.files.action.saveModel', localize('retry', "Retry"));
}
async run(): Promise<void> {
@ -301,7 +301,7 @@ class DiscardModelAction extends Action {
constructor(
private model: ITextFileEditorModel
) {
super('workbench.files.action.discardModel', nls.localize('discard', "Discard"));
super('workbench.files.action.discardModel', localize('discard', "Discard"));
}
async run(): Promise<void> {
@ -344,17 +344,17 @@ class SaveModelAsAction extends Action {
}
}
class OverwriteReadonlyModelAction extends Action {
class UnlockModelAction extends Action {
constructor(
private model: ITextFileEditorModel
) {
super('workbench.files.action.overwrite', nls.localize('overwrite', "Overwrite"));
super('workbench.files.action.unlock', localize('overwrite', "Overwrite"));
}
async run(): Promise<void> {
if (!this.model.isDisposed()) {
await this.model.save({ overwriteReadonly: true, reason: SaveReason.EXPLICIT });
await this.model.save({ writeUnlock: true, reason: SaveReason.EXPLICIT });
}
}
}
@ -364,7 +364,7 @@ class SaveModelIgnoreModifiedSinceAction extends Action {
constructor(
private model: ITextFileEditorModel
) {
super('workbench.files.action.saveIgnoreModifiedSince', nls.localize('overwrite', "Overwrite"));
super('workbench.files.action.saveIgnoreModifiedSince', localize('overwrite', "Overwrite"));
}
async run(): Promise<void> {
@ -379,7 +379,7 @@ class ConfigureSaveConflictAction extends Action {
constructor(
@IPreferencesService private readonly preferencesService: IPreferencesService
) {
super('workbench.files.action.configureSaveConflict', nls.localize('configure', "Configure"));
super('workbench.files.action.configureSaveConflict', localize('configure', "Configure"));
}
async run(): Promise<void> {

View file

@ -170,6 +170,12 @@ export class ExplorerService implements IExplorerService {
return this.model.findClosest(resource);
}
findClosestRoot(resource: URI): ExplorerItem | null {
const parentRoots = this.model.roots.filter(r => this.uriIdentityService.extUri.isEqualOrParent(resource, r.resource))
.sort((first, second) => second.resource.path.length - first.resource.path.length);
return parentRoots.length ? parentRoots[0] : null;
}
async setEditable(stat: ExplorerItem, data: IEditableData | null): Promise<void> {
if (!this.view) {
return;
@ -221,16 +227,13 @@ export class ExplorerService implements IExplorerService {
// Stat needs to be resolved first and then revealed
const options: IResolveFileOptions = { resolveTo: [resource], resolveMetadata: this.sortOrder === SortOrder.Modified };
const workspaceFolder = this.contextService.getWorkspaceFolder(resource);
if (workspaceFolder === null) {
return Promise.resolve(undefined);
const root = this.findClosestRoot(resource);
if (!root) {
return undefined;
}
const rootUri = workspaceFolder.uri;
const root = this.roots.find(r => this.uriIdentityService.extUri.isEqual(r.resource, rootUri))!;
try {
const stat = await this.fileService.resolve(rootUri, options);
const stat = await this.fileService.resolve(root.resource, options);
// Convert to model
const modelStat = ExplorerItem.create(this.fileService, stat, undefined, options.resolveTo);

View file

@ -84,6 +84,7 @@ export interface IExplorerService {
// If undefined is passed checks if any element is currently being edited.
isEditable(stat: ExplorerItem | undefined): boolean;
findClosest(resource: URI): ExplorerItem | null;
findClosestRoot(resource: URI): ExplorerItem | null;
refresh(): Promise<void>;
setToCopy(stats: ExplorerItem[], cut: boolean): Promise<void>;
isCut(stat: ExplorerItem): boolean;

View file

@ -703,9 +703,7 @@ export class ExplorerView extends ViewPane {
}
// Expand all stats in the parent chain.
let item: ExplorerItem | undefined = this.explorerService.roots.filter(i => this.uriIdentityService.extUri.isEqualOrParent(resource, i.resource))
// Take the root that is the closest to the stat #72299
.sort((first, second) => second.resource.path.length - first.resource.path.length)[0];
let item: ExplorerItem | null = this.explorerService.findClosestRoot(resource);
while (item && item.resource.toString() !== resource.toString()) {
try {
@ -719,7 +717,7 @@ export class ExplorerView extends ViewPane {
item = child;
break;
}
item = undefined;
item = null;
}
}

View file

@ -532,8 +532,7 @@ interface CachedParsedExpression {
* Makes sure that visible editors are always shown in the explorer even if they are filtered out by settings.
*/
export class FilesFilter implements ITreeFilter<ExplorerItem, FuzzyScore> {
private hiddenExpressionPerRoot: Map<string, CachedParsedExpression>;
private uriVisibilityMap = new Map<URI, boolean>();
private hiddenExpressionPerRoot = new Map<string, CachedParsedExpression>();
private editorsAffectingFilter = new Set<IEditorInput>();
private _onDidChange = new Emitter<void>();
private toDispose: IDisposable[] = [];
@ -545,7 +544,6 @@ export class FilesFilter implements ITreeFilter<ExplorerItem, FuzzyScore> {
@IEditorService private readonly editorService: IEditorService,
@IUriIdentityService private readonly uriIdentityService: IUriIdentityService
) {
this.hiddenExpressionPerRoot = new Map<string, CachedParsedExpression>();
this.toDispose.push(this.contextService.onDidChangeWorkspaceFolders(() => this.updateConfiguration()));
this.toDispose.push(this.configurationService.onDidChangeConfiguration((e) => {
if (e.affectsConfiguration('files.exclude')) {
@ -555,26 +553,30 @@ export class FilesFilter implements ITreeFilter<ExplorerItem, FuzzyScore> {
this.toDispose.push(this.editorService.onDidVisibleEditorsChange(() => {
const editors = this.editorService.visibleEditors;
let shouldFire = false;
this.uriVisibilityMap.forEach((visible, uri) => {
if (!visible) {
editors.forEach(e => {
if (e.resource && this.uriIdentityService.extUri.isEqualOrParent(e.resource, uri)) {
// A filtered resource suddenly became visible since user opened an editor
shouldFire = true;
}
});
}
});
this.editorsAffectingFilter.forEach(e => {
for (const e of editors) {
if (!e.resource) {
continue;
}
const stat = this.explorerService.findClosest(e.resource);
if (stat && stat.isExcluded) {
// A filtered resource suddenly became visible since user opened an editor
shouldFire = true;
break;
}
}
for (const e of this.editorsAffectingFilter) {
if (!editors.includes(e)) {
// Editor that was affecting filtering is no longer visible
shouldFire = true;
break;
}
});
}
if (shouldFire) {
this.editorsAffectingFilter.clear();
this.uriVisibilityMap.clear();
this._onDidChange.fire();
}
}));
@ -603,21 +605,12 @@ export class FilesFilter implements ITreeFilter<ExplorerItem, FuzzyScore> {
if (shouldFire) {
this.editorsAffectingFilter.clear();
this.uriVisibilityMap.clear();
this._onDidChange.fire();
}
}
filter(stat: ExplorerItem, parentVisibility: TreeVisibility): boolean {
const cachedVisibility = this.uriVisibilityMap.get(stat.resource);
if (typeof cachedVisibility === 'boolean') {
return cachedVisibility;
}
const isVisible = this.isVisible(stat, parentVisibility);
this.uriVisibilityMap.set(stat.resource, isVisible);
return isVisible;
return this.isVisible(stat, parentVisibility);
}
private isVisible(stat: ExplorerItem, parentVisibility: TreeVisibility): boolean {
@ -636,7 +629,7 @@ export class FilesFilter implements ITreeFilter<ExplorerItem, FuzzyScore> {
stat.isExcluded = true;
const editors = this.editorService.visibleEditors;
const editor = editors.find(e => e.resource && this.uriIdentityService.extUri.isEqualOrParent(e.resource, stat.resource));
if (editor) {
if (editor && stat.root === this.explorerService.findClosestRoot(stat.resource)) {
this.editorsAffectingFilter.add(editor);
return true; // Show all opened files and their parents
}

View file

@ -405,7 +405,7 @@ export class MarkersView extends ViewPane implements IMarkersView {
overrideStyles: {
listBackground: this.getBackgroundColor()
},
openOnFocus: true
selectionNavigation: true
},
));

View file

@ -32,3 +32,5 @@ export const EDITOR_BOTTOM_PADDING_WITHOUT_STATUSBAR = 12;
export const CELL_OUTPUT_PADDING = 14;
export const COLLAPSED_INDICATOR_HEIGHT = 24;
export const MARKDOWN_PREVIEW_PADDING = 8;

View file

@ -0,0 +1,38 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { RunOnceScheduler } from 'vs/base/common/async';
import { Disposable } from 'vs/base/common/lifecycle';
import { INotebookEditor, INotebookEditorContribution } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
import { registerNotebookContribution } from 'vs/workbench/contrib/notebook/browser/notebookEditorExtensions';
import { CellKind, cellRangesToIndexes } from 'vs/workbench/contrib/notebook/common/notebookCommon';
class NotebookClipboardContribution extends Disposable implements INotebookEditorContribution {
static id: string = 'workbench.notebook.viewportCustomMarkdown';
private readonly _warmupViewport: RunOnceScheduler;
constructor(private readonly _notebookEditor: INotebookEditor) {
super();
this._warmupViewport = new RunOnceScheduler(() => this._warmupViewportNow(), 200);
this._register(this._notebookEditor.onDidScroll(() => {
this._warmupViewport.schedule();
}));
}
private _warmupViewportNow() {
const visibleRanges = this._notebookEditor.getVisibleRangesPlusViewportAboveBelow();
cellRangesToIndexes(visibleRanges).forEach(index => {
const cell = this._notebookEditor.viewModel?.viewCells[index];
if (cell?.cellKind === CellKind.Markdown) {
this._notebookEditor.createMarkdownPreview(cell);
}
});
}
}
registerNotebookContribution(NotebookClipboardContribution.id, NotebookClipboardContribution);

View file

@ -41,7 +41,7 @@ import { generateUuid } from 'vs/base/common/uuid';
import { IMouseWheelEvent, StandardMouseEvent } from 'vs/base/browser/mouseEvent';
import { DiffNestedCellViewModel } from 'vs/workbench/contrib/notebook/browser/diff/diffNestedCellViewModel';
import { BackLayerWebView } from 'vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView';
import { CELL_OUTPUT_PADDING } from 'vs/workbench/contrib/notebook/browser/constants';
import { CELL_OUTPUT_PADDING, MARKDOWN_PREVIEW_PADDING } from 'vs/workbench/contrib/notebook/browser/constants';
import { NotebookDiffEditorEventDispatcher, NotebookDiffLayoutChangedEvent } from 'vs/workbench/contrib/notebook/browser/diff/eventDispatcher';
const $ = DOM.$;
@ -392,6 +392,7 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD
private readonly webviewOptions = {
outputNodePadding: CELL_OUTPUT_PADDING,
outputNodeLeftPadding: 32,
previewNodePadding: MARKDOWN_PREVIEW_PADDING,
leftMargin: 0,
cellMargin: 0,
runGutter: 0

View file

@ -67,6 +67,7 @@ import 'vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline';
import 'vs/workbench/contrib/notebook/browser/contrib/status/editorStatus';
import 'vs/workbench/contrib/notebook/browser/contrib/undoRedo/notebookUndoRedo';
import 'vs/workbench/contrib/notebook/browser/contrib/cellOperations/cellOperations';
import 'vs/workbench/contrib/notebook/browser/contrib/viewportCustomMarkdown/viewportCustomMarkdown';
// Diff Editor Contribution

View file

@ -242,6 +242,7 @@ export interface ICellViewModel extends IGenericCellViewModel {
outputIsHovered: boolean;
getText(): string;
getTextLength(): number;
getHeight(lineHeight: number): number;
metadata: NotebookCellMetadata | undefined;
textModel: ITextModel | undefined;
hasModel(): this is IEditableCellViewModel;
@ -354,6 +355,7 @@ export interface INotebookEditor extends ICommonNotebookEditor {
readonly onDidFocusEditorWidget: Event<void>;
activeKernel: INotebookKernel | undefined;
multipleKernelsAvailable: boolean;
readonly onDidScroll: Event<void>;
readonly onDidChangeAvailableKernels: Event<void>;
readonly onDidChangeKernel: Event<void>;
readonly onDidChangeActiveCell: Event<void>;
@ -386,6 +388,8 @@ export interface INotebookEditor extends ICommonNotebookEditor {
*/
getLayoutInfo(): NotebookLayoutInfo;
getVisibleRangesPlusViewportAboveBelow(): ICellRange[];
/**
* Fetch the output renderers for notebook outputs.
*/
@ -655,6 +659,7 @@ export interface INotebookCellList {
getViewIndex2(modelIndex: number): number | undefined;
getModelIndex(cell: CellViewModel): number | undefined;
getModelIndex2(viewIndex: number): number | undefined;
getVisibleRangesPlusViewportAboveBelow(): ICellRange[];
focusElement(element: ICellViewModel): void;
selectElement(element: ICellViewModel): void;
getFocusedElements(): ICellViewModel[];

View file

@ -42,7 +42,7 @@ import { IEditorMemento } from 'vs/workbench/common/editor';
import { Memento, MementoObject } from 'vs/workbench/common/memento';
import { PANEL_BORDER } from 'vs/workbench/common/theme';
import { debugIconStartForeground } from 'vs/workbench/contrib/debug/browser/debugColors';
import { BOTTOM_CELL_TOOLBAR_GAP, BOTTOM_CELL_TOOLBAR_HEIGHT, CELL_BOTTOM_MARGIN, CELL_MARGIN, CELL_OUTPUT_PADDING, CELL_RUN_GUTTER, CELL_TOP_MARGIN, CODE_CELL_LEFT_MARGIN, COLLAPSED_INDICATOR_HEIGHT, MARKDOWN_CELL_BOTTOM_MARGIN, MARKDOWN_CELL_TOP_MARGIN, SCROLLABLE_ELEMENT_PADDING_TOP } from 'vs/workbench/contrib/notebook/browser/constants';
import { BOTTOM_CELL_TOOLBAR_GAP, BOTTOM_CELL_TOOLBAR_HEIGHT, CELL_BOTTOM_MARGIN, CELL_MARGIN, CELL_OUTPUT_PADDING, CELL_RUN_GUTTER, CELL_TOP_MARGIN, CODE_CELL_LEFT_MARGIN, COLLAPSED_INDICATOR_HEIGHT, MARKDOWN_CELL_BOTTOM_MARGIN, MARKDOWN_CELL_TOP_MARGIN, MARKDOWN_PREVIEW_PADDING, SCROLLABLE_ELEMENT_PADDING_TOP } from 'vs/workbench/contrib/notebook/browser/constants';
import { CellEditState, CellFocusMode, IActiveNotebookEditor, ICellOutputViewModel, ICellViewModel, ICommonCellInfo, IDisplayOutputLayoutUpdateRequest, IFocusNotebookCellOptions, IGenericCellViewModel, IInsetRenderOutput, INotebookCellList, INotebookCellOutputLayoutInfo, INotebookDeltaDecoration, INotebookEditor, INotebookEditorContribution, INotebookEditorContributionDescription, INotebookEditorCreationOptions, INotebookEditorMouseEvent, NotebookEditorOptions, NotebookLayoutInfo, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_EDITOR_ID, NOTEBOOK_OUTPUT_FOCUSED } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
import { NotebookDecorationCSSRules, NotebookRefCountedStyleSheet } from 'vs/workbench/contrib/notebook/browser/notebookEditorDecorations';
import { NotebookEditorExtensionsRegistry } from 'vs/workbench/contrib/notebook/browser/notebookEditorExtensions';
@ -167,6 +167,8 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor
return this._renderedEditors.get(focused);
}
private readonly _onDidScroll = this._register(new Emitter<void>());
readonly onDidScroll: Event<void> = this._onDidScroll.event;
private readonly _onDidChangeActiveCell = this._register(new Emitter<void>());
readonly onDidChangeActiveCell: Event<void> = this._onDidChangeActiveCell.event;
private _cursorNavigationMode: boolean = false;
@ -281,6 +283,29 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor
}
this._updateForNotebookConfiguration();
if (this._debugFlag) {
this._domFrameLog();
}
}
private _debugFlag: boolean = false;
private _frameId = 0;
private _domFrameLog() {
DOM.scheduleAtNextAnimationFrame(() => {
this._frameId++;
this._domFrameLog();
});
}
private _debug(...args: any[]) {
if (!this._debugFlag) {
return;
}
const date = new Date();
console.log(`${date.getSeconds()}:${date.getMilliseconds().toString().padStart(3, '0')}`, `frame #${this._frameId}: `, ...args);
}
/**
@ -486,6 +511,10 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor
this._onDidChangeVisibleRanges.fire();
}));
this._register(this._list.onDidScroll(() => {
this._onDidScroll.fire();
}));
const widgetFocusTracker = DOM.trackFocus(this.getDomNode());
this._register(widgetFocusTracker);
this._register(widgetFocusTracker.onDidFocus(() => this._onDidFocusEmitter.fire()));
@ -659,7 +688,10 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor
private async _loadKernelPreloads(extensionLocation: URI, kernel: INotebookKernel) {
if (kernel.preloads && kernel.preloads.length) {
await this._resolveWebview();
if (!this._webview?.isResolved()) {
await this._resolveWebview();
}
this._webview?.updateKernelPreloads([extensionLocation], kernel.preloads.map(preload => URI.revive(preload)));
}
}
@ -734,6 +766,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor
this._webview = this.instantiationService.createInstance(BackLayerWebView, this, id, resource, {
outputNodePadding: CELL_OUTPUT_PADDING,
outputNodeLeftPadding: CELL_OUTPUT_PADDING,
previewNodePadding: MARKDOWN_PREVIEW_PADDING,
leftMargin: CODE_CELL_LEFT_MARGIN,
cellMargin: CELL_MARGIN,
runGutter: CELL_RUN_GUTTER,
@ -756,19 +789,6 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor
this._updateForMetadata();
}));
const useRenderer = this.configurationService.getValue<string>('notebook.experimental.useMarkdownRenderer');
if (useRenderer) {
await this._resolveWebview();
await this._webview!.initializeMarkdown(this.viewModel.viewCells
.filter(cell => cell.cellKind === CellKind.Markdown)
.map(cell => ({ cellId: cell.id, content: cell.getText() }))
// TODO: look at cell position cache instead of just getting first five cells
.slice(0, 5));
}
// restore view states, including contributions
{
@ -849,20 +869,26 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor
removedItems.forEach(output => this._webview?.removeInset(output));
if (updateItems.length) {
this._debug('_list.onDidChangeContentHeight/outputs', updateItems);
this._webview?.updateViewScrollTop(-scrollTop, false, updateItems);
}
}
if (this._webview?.markdownPreviewMapping) {
const updateItems: { id: string, top: number }[] = [];
this._webview!.markdownPreviewMapping.forEach(cellId => {
this._webview.markdownPreviewMapping.forEach((_, cellId) => {
const cell = this.viewModel?.viewCells.find(cell => cell.id === cellId);
if (cell) {
const cellTop = this._list.getAbsoluteTopOfElement(cell);
updateItems.push({ id: cellId, top: cellTop });
}
});
this._webview?.updateMarkdownScrollTop(updateItems);
if (updateItems.length) {
this._debug('_list.onDidChangeContentHeight/markdown', updateItems);
this._webview?.updateMarkdownScrollTop(updateItems);
}
}
});
}));
@ -875,11 +901,25 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor
}));
this._localStore.add(this._list.onDidRemoveCellFromView(cell => {
if (cell.cellKind === CellKind.Markdown) {
this.removeMarkdownPreview(cell as MarkdownCellViewModel);
const mdCell = cell as MarkdownCellViewModel;
if (this.viewModel?.viewCells.find(cell => cell.handle === mdCell.handle)) {
// Cell has been folded but is still in model
this.hideMarkdownPreview(mdCell);
} else {
// Cell was deleted
this.removeMarkdownPreview(mdCell);
}
}
}));
this._list.attachViewModel(this.viewModel);
// init rendering
const useRenderer = this.configurationService.getValue<string>('notebook.experimental.useMarkdownRenderer');
if (useRenderer) {
await this._warmupWithMarkdownRenderer(this.viewModel, viewState);
} else {
this._list.attachViewModel(this.viewModel);
}
if (this._dimension) {
this._list.layout(this._dimension.height - SCROLLABLE_ELEMENT_PADDING_TOP, this._dimension.width);
@ -893,6 +933,91 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor
this.restoreListViewState(viewState);
}
private async _warmupWithMarkdownRenderer(viewModel: NotebookViewModel, viewState: INotebookEditorViewState | undefined) {
await this._resolveWebview();
// make sure that the webview is not visible otherwise users will see pre-rendered markdown cells in wrong position as the list view doesn't have a correct `top` offset yet
this._webview!.element.style.visibility = 'hidden';
// warm up can take around 200ms to load markdown libraries, etc.
await this._warmupViewport(viewModel, viewState);
// todo@rebornix @mjbvz, is this too complicated?
/* now the webview is ready, and requests to render markdown are fast enough
* we can start rendering the list view
* render
* - markdown cell -> request to webview to (10ms, basically just latency between UI and iframe)
* - code cell -> render in place
*/
this._list.layout(0, 0);
this._list.attachViewModel(viewModel);
// now the list widget has a correct contentHeight/scrollHeight
// setting scrollTop will work properly
// after setting scroll top, the list view will update `top` of the scrollable element, e.g. `top: -584px`
this._list.scrollTop = viewState?.scrollPosition?.top ?? 0;
this._debug('finish initial viewport warmup and view state restore.');
this._webview!.element.style.visibility = 'visible';
}
private async _warmupViewport(viewModel: NotebookViewModel, viewState: INotebookEditorViewState | undefined) {
if (viewState && viewState.cellTotalHeights) {
const totalHeightCache = viewState.cellTotalHeights;
const scrollTop = viewState.scrollPosition?.top ?? 0;
const scrollBottom = scrollTop + Math.max(this._dimension?.height ?? 0, 1080);
let offset = 0;
let requests: [ICellViewModel, number][] = [];
for (let i = 0; i < viewModel.viewCells.length; i++) {
const cell = viewModel.viewCells[i];
if (offset + (totalHeightCache[i] ?? 0) < scrollTop) {
offset += (totalHeightCache ? totalHeightCache[i] : 0);
continue;
} else {
if (cell.cellKind === CellKind.Markdown) {
requests.push([cell, offset]);
}
}
offset += (totalHeightCache ? totalHeightCache[i] : 0);
if (offset > scrollBottom) {
break;
}
}
await this._webview!.initializeMarkdown(requests
.map(request => ({ cellId: request[0].id, cellHandle: request[0].handle, content: request[0].getText(), offset: request[1] })));
} else {
const initRequests = viewModel.viewCells.filter(cell => cell.cellKind === CellKind.Markdown).slice(0, 5).map(cell => ({ cellId: cell.id, cellHandle: cell.handle, content: cell.getText(), offset: -10000 }));
await this._webview!.initializeMarkdown(initRequests);
// no cached view state so we are rendering the first viewport
// after above async call, we already get init height for markdown cells, we can update their offset
let offset = 0;
let offsetUpdateRequests: { id: string, top: number }[] = [];
const scrollBottom = Math.max(this._dimension?.height ?? 0, 1080);
for (let i = 0; i < viewModel.viewCells.length; i++) {
const cell = viewModel.viewCells[i];
if (cell.cellKind === CellKind.Markdown) {
offsetUpdateRequests.push({ id: cell.id, top: offset });
}
offset += cell.getHeight(this.getLayoutInfo().fontInfo.lineHeight);
if (offset > scrollBottom) {
break;
}
}
this._webview?.updateMarkdownScrollTop(offsetUpdateRequests);
}
}
restoreListViewState(viewState: INotebookEditorViewState | undefined): void {
if (viewState?.scrollPosition !== undefined) {
this._list.scrollTop = viewState!.scrollPosition.top;
@ -1215,6 +1340,10 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor
return this._list.setHiddenAreas(_ranges, true);
}
getVisibleRangesPlusViewportAboveBelow(): ICellRange[] {
return this._list?.getVisibleRangesPlusViewportAboveBelow() ?? [];
}
//#endregion
//#region Decorations
@ -1350,6 +1479,10 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor
return;
}
if (this._list.elementHeight(cell) === height) {
return;
}
this._pendingLayouts.delete(cell);
relayout(cell, height);
@ -1558,6 +1691,8 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor
throw new Error('Notebook Editor move cell, index out of range');
}
// this._list.move(index, desiredIndex);
let r: (val: ICellViewModel | null) => void;
DOM.scheduleAtNextAnimationFrame(() => {
if (this._isDisposed) {
@ -1680,22 +1815,26 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor
}
async createMarkdownPreview(cell: MarkdownCellViewModel) {
const useRenderer = this.configurationService.getValue<string>('notebook.experimental.useMarkdownRenderer');
if (!useRenderer) {
// TODO: handle case where custom renderer is disabled?
return;
}
if (!this._webview) {
return;
}
await this._resolveWebview();
if (!this._webview.isResolved()) {
await this._resolveWebview();
}
if (!this._webview) {
return;
}
const cellTop = this._list.getAbsoluteTopOfElement(cell);
if (this._webview.markdownPreviewMapping.has(cell.id)) {
await this._webview.showMarkdownPreview(cell.id, cell.getText(), cellTop);
} else {
await this._webview.createMarkdownPreview(cell.id, cell.getText(), cellTop);
}
await this._webview.showMarkdownPreview(cell.id, cell.handle, cell.getText(), cellTop, cell.version);
}
async unhideMarkdownPreview(cell: MarkdownCellViewModel) {
@ -1703,7 +1842,9 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor
return;
}
await this._resolveWebview();
if (!this._webview.isResolved()) {
await this._resolveWebview();
}
await this._webview?.unhideMarkdownPreview(cell.id);
}
@ -1713,7 +1854,9 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor
return;
}
await this._resolveWebview();
if (!this._webview.isResolved()) {
await this._resolveWebview();
}
await this._webview?.hideMarkdownPreview(cell.id);
}
@ -1723,7 +1866,9 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor
return;
}
await this._resolveWebview();
if (!this._webview.isResolved()) {
await this._resolveWebview();
}
await this._webview?.removeMarkdownPreview(cell.id);
}
@ -1733,7 +1878,9 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor
return;
}
await this._resolveWebview();
if (!this._webview.isResolved()) {
await this._resolveWebview();
}
await this._webview?.updateMarkdownPreviewSelectionState(cell.id, isSelected);
}
@ -1744,7 +1891,9 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor
return;
}
await this._resolveWebview();
if (!this._webview.isResolved()) {
await this._resolveWebview();
}
if (!this._webview!.insetMapping.has(output.source)) {
const cellTop = this._list.getAbsoluteTopOfElement(cell);
@ -1819,7 +1968,10 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor
updateMarkdownCellHeight(cellId: string, height: number, isInit: boolean) {
const cell = this.getCellById(cellId);
if (cell && cell instanceof MarkdownCellViewModel) {
cell.renderedMarkdownHeight = height;
if (height + BOTTOM_CELL_TOOLBAR_GAP !== cell.layoutInfo.totalHeight) {
this._debug('updateMarkdownCellHeight', cell.handle, height + BOTTOM_CELL_TOOLBAR_GAP, isInit);
cell.renderedMarkdownHeight = height;
}
}
}

View file

@ -203,48 +203,7 @@ export class NotebookCellList extends WorkbenchList<CellViewModel> implements ID
if (bottomModelIndex - topModelIndex === bottomViewIndex - topViewIndex) {
this.visibleRanges = [{ start: topModelIndex, end: bottomModelIndex }];
} else {
let stack: number[] = [];
const ranges: ICellRange[] = [];
// there are hidden ranges
let index = topViewIndex;
let modelIndex = topModelIndex;
while (index <= bottomViewIndex) {
const accu = this.hiddenRangesPrefixSum!.getAccumulatedValue(index);
if (accu === modelIndex + 1) {
// no hidden area after it
if (stack.length) {
if (stack[stack.length - 1] === modelIndex - 1) {
ranges.push({ start: stack[stack.length - 1], end: modelIndex });
} else {
ranges.push({ start: stack[stack.length - 1], end: stack[stack.length - 1] });
}
}
stack.push(modelIndex);
index++;
modelIndex++;
} else {
// there are hidden ranges after it
if (stack.length) {
if (stack[stack.length - 1] === modelIndex - 1) {
ranges.push({ start: stack[stack.length - 1], end: modelIndex });
} else {
ranges.push({ start: stack[stack.length - 1], end: stack[stack.length - 1] });
}
}
stack.push(modelIndex);
index++;
modelIndex = accu;
}
}
if (stack.length) {
ranges.push({ start: stack[stack.length - 1], end: stack[stack.length - 1] });
}
this.visibleRanges = reduceCellRanges(ranges);
this.visibleRanges = this._getVisibleRangesFromIndex(topViewIndex, topModelIndex, bottomViewIndex, bottomModelIndex);
}
};
@ -533,6 +492,72 @@ export class NotebookCellList extends WorkbenchList<CellViewModel> implements ID
}
}
private _getVisibleRangesFromIndex(topViewIndex: number, topModelIndex: number, bottomViewIndex: number, bottomModelIndex: number) {
let stack: number[] = [];
const ranges: ICellRange[] = [];
// there are hidden ranges
let index = topViewIndex;
let modelIndex = topModelIndex;
while (index <= bottomViewIndex) {
const accu = this.hiddenRangesPrefixSum!.getAccumulatedValue(index);
if (accu === modelIndex + 1) {
// no hidden area after it
if (stack.length) {
if (stack[stack.length - 1] === modelIndex - 1) {
ranges.push({ start: stack[stack.length - 1], end: modelIndex });
} else {
ranges.push({ start: stack[stack.length - 1], end: stack[stack.length - 1] });
}
}
stack.push(modelIndex);
index++;
modelIndex++;
} else {
// there are hidden ranges after it
if (stack.length) {
if (stack[stack.length - 1] === modelIndex - 1) {
ranges.push({ start: stack[stack.length - 1], end: modelIndex });
} else {
ranges.push({ start: stack[stack.length - 1], end: stack[stack.length - 1] });
}
}
stack.push(modelIndex);
index++;
modelIndex = accu;
}
}
if (stack.length) {
ranges.push({ start: stack[stack.length - 1], end: stack[stack.length - 1] });
}
return reduceCellRanges(ranges);
}
getVisibleRangesPlusViewportAboveBelow() {
if (this.view.length <= 0) {
return [];
}
const top = clamp(this.getViewScrollTop() - this.renderHeight, 0, this.scrollHeight);
const bottom = clamp(this.getViewScrollBottom() + this.renderHeight, 0, this.scrollHeight);
const topViewIndex = clamp(this.view.indexAt(top), 0, this.view.length - 1);
const topElement = this.view.element(topViewIndex);
const topModelIndex = this._viewModel!.getCellIndex(topElement);
const bottomViewIndex = clamp(this.view.indexAt(bottom), 0, this.view.length - 1);
const bottomElement = this.view.element(bottomViewIndex);
const bottomModelIndex = this._viewModel!.getCellIndex(bottomElement);
if (bottomModelIndex - topModelIndex === bottomViewIndex - topViewIndex) {
return [{ start: topModelIndex, end: bottomModelIndex }];
} else {
return this._getVisibleRangesFromIndex(topViewIndex, topModelIndex, bottomViewIndex, bottomModelIndex);
}
}
private _getViewIndexUpperBound(cell: ICellViewModel): number {
if (!this._viewModel) {
return -1;

View file

@ -236,6 +236,7 @@ export interface ICustomRendererMessage extends BaseToWebviewMessage {
export interface ICreateMarkdownMessage {
type: 'createMarkdownPreview',
id: string;
handle: number;
content: string;
top: number;
}
@ -257,7 +258,8 @@ export interface IUnhideMarkdownMessage {
export interface IShowMarkdownMessage {
type: 'showMarkdownPreview',
id: string;
content: string;
handle: number;
content: string | undefined;
top: number;
}
@ -269,7 +271,7 @@ export interface IUpdateMarkdownPreviewSelectionState {
export interface IInitializeMarkdownMessage {
type: 'initializeMarkdownPreview';
cells: Array<{ cellId: string, content: string }>;
cells: Array<{ cellId: string, cellHandle: number, content: string, offset: number }>;
}
export type FromWebviewMessage =
@ -343,7 +345,7 @@ export class BackLayerWebView<T extends ICommonCellInfo> extends Disposable {
element: HTMLElement;
webview: WebviewElement | undefined = undefined;
insetMapping: Map<IDisplayOutputViewModel, ICachedInset<T>> = new Map();
markdownPreviewMapping: Set<string> = new Set();
markdownPreviewMapping = new Map<string, { version: number, visible: boolean }>();
hiddenInsetMapping: Set<IDisplayOutputViewModel> = new Set();
reversedInsetMapping: Map<string, IDisplayOutputViewModel> = new Map();
localResourceRootsCache: URI[] | undefined = undefined;
@ -363,6 +365,7 @@ export class BackLayerWebView<T extends ICommonCellInfo> extends Disposable {
public options: {
outputNodePadding: number,
outputNodeLeftPadding: number,
previewNodePadding: number,
leftMargin: number,
cellMargin: number,
runGutter: number,
@ -415,6 +418,8 @@ export class BackLayerWebView<T extends ICommonCellInfo> extends Disposable {
color: var(--vscode-foreground);
width: calc(100% - ${this.options.cellMargin}px);
padding-left: ${this.options.leftMargin}px;
padding-top: ${this.options.previewNodePadding}px;
padding-bottom: ${this.options.previewNodePadding}px;
}
#container > div > div.preview.selected {
@ -449,11 +454,20 @@ export class BackLayerWebView<T extends ICommonCellInfo> extends Disposable {
}
#container > div > div.preview h1 {
padding-bottom: 0.3em;
line-height: 1.2;
font-size: 26px;
padding-bottom: 8px;
line-height: 31px;
border-bottom-width: 1px;
border-bottom-style: solid;
border-color: var(--vscode-foreground);
margin: 0;
margin-bottom: 13px;
}
#container > div > div.preview h2 {
font-size: 19px;
margin: 0;
margin-bottom: 10px;
}
#container > div > div.preview h1,
@ -485,7 +499,7 @@ export class BackLayerWebView<T extends ICommonCellInfo> extends Disposable {
/* makes all markdown cells consistent */
#container > div > div.preview div {
min-height: 24px;
min-height: ${this.options.previewNodePadding * 2}px;
}
#container > div > div.preview table {
@ -620,7 +634,7 @@ export class BackLayerWebView<T extends ICommonCellInfo> extends Disposable {
<script>${preloadsScriptStr({
outputNodePadding: this.options.outputNodePadding,
outputNodeLeftPadding: this.options.outputNodeLeftPadding,
previewNodePadding: 8,
previewNodePadding: this.options.previewNodePadding,
leftMargin: this.options.leftMargin
})}</script>
${markdownRenderersSrc}
@ -1051,33 +1065,57 @@ var requirejs = (function() {
});
}
async createMarkdownPreview(cellId: string, content: string, cellTop: number) {
private async createMarkdownPreview(cellId: string, cellHandle: number, content: string, cellTop: number, contentVersion: number) {
if (this._disposed) {
return;
}
if (this.markdownPreviewMapping.has(cellId)) {
console.error('Trying to create markdown preview that already exists');
return;
}
const initialTop = cellTop;
this.markdownPreviewMapping.add(cellId);
this.markdownPreviewMapping.set(cellId, { version: contentVersion, visible: true });
this._sendMessageToWebview({
type: 'createMarkdownPreview',
id: cellId,
handle: cellHandle,
content: content,
top: initialTop,
});
}
async showMarkdownPreview(cellId: string, content: string, cellTop: number) {
async showMarkdownPreview(cellId: string, cellHandle: number, content: string, cellTop: number, contentVersion: number) {
if (this._disposed) {
return;
}
this._sendMessageToWebview({
type: 'showMarkdownPreview',
id: cellId,
content: content,
top: cellTop
});
if (!this.markdownPreviewMapping.has(cellId)) {
return this.createMarkdownPreview(cellId, cellHandle, content, cellTop, contentVersion);
}
const entry = this.markdownPreviewMapping.get(cellId);
if (!entry) {
console.error('Try to show a preview that does not exist');
return;
}
if (entry.version !== contentVersion || !entry.visible) {
this._sendMessageToWebview({
type: 'showMarkdownPreview',
id: cellId,
handle: cellHandle,
// If the content has not changed, we still want to make sure the
// preview is visible but don't need to send anything over
content: entry.version === contentVersion ? undefined : content,
top: cellTop
});
}
entry.version = contentVersion;
entry.visible = true;
}
async hideMarkdownPreview(cellId: string,) {
@ -1085,10 +1123,20 @@ var requirejs = (function() {
return;
}
this._sendMessageToWebview({
type: 'hideMarkdownPreview',
id: cellId
});
const entry = this.markdownPreviewMapping.get(cellId);
if (!entry) {
// TODO: this currently seems expected on first load
// console.error(`Try to hide a preview that does not exist: ${cellId}`);
return;
}
if (entry.visible) {
this._sendMessageToWebview({
type: 'hideMarkdownPreview',
id: cellId
});
entry.visible = false;
}
}
async unhideMarkdownPreview(cellId: string,) {
@ -1096,10 +1144,19 @@ var requirejs = (function() {
return;
}
this._sendMessageToWebview({
type: 'unhideMarkdownPreview',
id: cellId
});
const entry = this.markdownPreviewMapping.get(cellId);
if (!entry) {
console.error(`Try to unhide a preview that does not exist: ${cellId}`);
return;
}
if (!entry.visible) {
this._sendMessageToWebview({
type: 'unhideMarkdownPreview',
id: cellId
});
entry.visible = true;
}
}
async removeMarkdownPreview(cellId: string,) {
@ -1107,6 +1164,11 @@ var requirejs = (function() {
return;
}
if (!this.markdownPreviewMapping.has(cellId)) {
console.error(`Try to delete a preview that does not exist: ${cellId}`);
return;
}
this.markdownPreviewMapping.delete(cellId);
this._sendMessageToWebview({
@ -1120,15 +1182,9 @@ var requirejs = (function() {
return;
}
this._sendMessageToWebview({
type: 'updateMarkdownPreviewSelectionState',
id: cellId,
isSelected
});
}
async updateMarkdownPreviewDecpratopms(cellId: any, isSelected: boolean) {
if (this._disposed) {
if (!this.markdownPreviewMapping.has(cellId)) {
// TODO: this currently seems expected on first load
// console.error(`Try to update selection state for preview that does not exist: ${cellId}`);
return;
}
@ -1139,9 +1195,13 @@ var requirejs = (function() {
});
}
async initializeMarkdown(cells: Array<{ cellId: string, content: string }>) {
async initializeMarkdown(cells: Array<{ cellId: string, cellHandle: number, content: string, offset: number }>) {
await this._loaded;
if (this._disposed) {
return;
}
// TODO: use proper handler
const p = new Promise<void>(resolve => {
this.webview?.onMessage(e => {
@ -1152,7 +1212,7 @@ var requirejs = (function() {
});
for (const cell of cells) {
this.markdownPreviewMapping.add(cell.cellId);
this.markdownPreviewMapping.set(cell.cellId, { version: 0, visible: false });
}
this._sendMessageToWebview({

View file

@ -144,25 +144,34 @@ function webviewPreloads() {
}
if (entry.target.id === id && entry.contentRect) {
if (entry.contentRect.height !== 0) {
const padding = output ? __outputNodePadding__ : __previewNodePadding__;
entry.target.style.padding = `${padding}px ${padding}px ${padding}px ${output ? __outputNodeLeftPadding__ : __leftMargin__}px`;
postNotebookMessage<IDimensionMessage>('dimension', {
id: id,
data: {
height: entry.contentRect.height + padding * 2
},
isOutput: output
});
if (output) {
if (entry.contentRect.height !== 0) {
entry.target.style.padding = `${__outputNodePadding__}px ${__outputNodePadding__}px ${__outputNodePadding__}px ${output ? __outputNodeLeftPadding__ : __leftMargin__}px`;
postNotebookMessage<IDimensionMessage>('dimension', {
id: id,
data: {
height: entry.contentRect.height + __outputNodePadding__ * 2
},
isOutput: true
});
} else {
entry.target.style.padding = `0px`;
postNotebookMessage<IDimensionMessage>('dimension', {
id: id,
data: {
height: entry.contentRect.height
},
isOutput: true
});
}
} else {
entry.target.style.padding = `0px`;
postNotebookMessage<IDimensionMessage>('dimension', {
id: id,
data: {
height: entry.contentRect.height
// entry.contentRect does not include padding
height: entry.contentRect.height + __previewNodePadding__ * 2
},
isOutput: output
isOutput: false
});
}
}
@ -403,7 +412,12 @@ function webviewPreloads() {
switch (event.data.type) {
case 'initializeMarkdownPreview':
for (const cell of event.data.cells) {
createMarkdownPreview(cell.cellId, cell.content, -10000);
createMarkdownPreview(cell.cellId, cell.content, cell.offset);
const cellContainer = document.getElementById(cell.cellId);
if (cellContainer) {
cellContainer.style.visibility = 'hidden';
}
}
postNotebookMessage('initializedMarkdownPreview', {});
@ -414,15 +428,18 @@ function webviewPreloads() {
case 'showMarkdownPreview':
{
const data = event.data;
let cellContainer = document.getElementById(data.id);
if (cellContainer) {
cellContainer.style.display = 'block';
}
const previewNode = document.getElementById(`${data.id}_container`);
if (previewNode) {
previewNode.style.top = `${data.top}px`;
}
updateMarkdownPreview(data.id, data.content);
const cellContainer = document.getElementById(data.id);
if (cellContainer) {
cellContainer.style.visibility = 'visible';
}
if (typeof data.content === 'string') {
updateMarkdownPreview(data.id, data.content);
}
}
break;
case 'hideMarkdownPreview':
@ -430,7 +447,7 @@ function webviewPreloads() {
const data = event.data;
const cellContainer = document.getElementById(data.id);
if (cellContainer) {
cellContainer.style.display = 'none';
cellContainer.style.visibility = 'hidden';
}
}
break;
@ -439,7 +456,7 @@ function webviewPreloads() {
const data = event.data;
const cellContainer = document.getElementById(data.id);
if (cellContainer) {
cellContainer.style.display = '';
cellContainer.style.visibility = 'visible';
}
}
break;
@ -574,7 +591,7 @@ function webviewPreloads() {
case 'view-scroll-markdown':
{
// const date = new Date();
// console.log('----- will scroll ---- ', date.getMinutes() + ':' + date.getSeconds() + ':' + date.getMilliseconds());
// console.log(`${date.getSeconds()}:${date.getMilliseconds().toString().padStart(3, '0')}`, '[iframe]: view-scroll-markdown', event.data.cells);
event.data.cells.map(cell => {
const widget = document.getElementById(`${cell.id}_preview`)!;
@ -582,6 +599,11 @@ function webviewPreloads() {
widget.style.top = `${cell.top}px`;
}
const markdownPreview = document.getElementById(`${cell.id}`);
if (markdownPreview) {
markdownPreview.style.display = 'block';
}
});
break;
@ -698,88 +720,82 @@ function webviewPreloads() {
});
function createMarkdownPreview(cellId: string, content: string, top: number) {
let cellContainer = document.getElementById(cellId);
if (!cellContainer) {
const container = document.getElementById('container')!;
const newElement = document.createElement('div');
const container = document.getElementById('container')!;
const cellContainer = document.createElement('div');
newElement.id = `${cellId}`;
container.appendChild(newElement);
cellContainer = newElement;
cellContainer.id = `${cellId}`;
container.appendChild(cellContainer);
const previewContainerNode = document.createElement('div');
previewContainerNode.style.position = 'absolute';
previewContainerNode.style.top = top + 'px';
previewContainerNode.id = `${cellId}_preview`;
previewContainerNode.classList.add('preview');
const previewContainerNode = document.createElement('div');
previewContainerNode.style.position = 'absolute';
previewContainerNode.style.top = top + 'px';
previewContainerNode.id = `${cellId}_preview`;
previewContainerNode.classList.add('preview');
previewContainerNode.addEventListener('dblclick', () => {
postNotebookMessage<IToggleMarkdownPreviewMessage>('toggleMarkdownPreview', { cellId });
previewContainerNode.addEventListener('dblclick', () => {
postNotebookMessage<IToggleMarkdownPreviewMessage>('toggleMarkdownPreview', { cellId });
});
previewContainerNode.addEventListener('click', () => {
postNotebookMessage<IFocusMarkdownPreviewMessage>('focusMarkdownPreview', { cellId });
});
previewContainerNode.addEventListener('mouseenter', () => {
postNotebookMessage<IMouseEnterMarkdownPreviewMessage>('mouseEnterMarkdownPreview', { cellId });
});
previewContainerNode.addEventListener('mouseleave', () => {
postNotebookMessage<IMouseLeaveMarkdownPreviewMessage>('mouseLeaveMarkdownPreview', { cellId });
});
previewContainerNode.setAttribute('draggable', 'true');
previewContainerNode.addEventListener('dragstart', e => {
if (!e.dataTransfer) {
return;
}
e.dataTransfer.setData(markdownCellDragDataType, JSON.stringify({ cellId }));
(e.target as HTMLElement).classList.add('dragging');
postNotebookMessage<ICellDragStartMessage>('cell-drag-start', {
cellId: cellId,
position: { clientX: e.clientX, clientY: e.clientY },
});
});
previewContainerNode.addEventListener('click', () => {
postNotebookMessage<IFocusMarkdownPreviewMessage>('focusMarkdownPreview', { cellId });
previewContainerNode.addEventListener('drag', e => {
postNotebookMessage<ICellDragMessage>('cell-drag', {
cellId: cellId,
position: { clientX: e.clientX, clientY: e.clientY },
});
});
previewContainerNode.addEventListener('mouseenter', () => {
postNotebookMessage<IMouseEnterMarkdownPreviewMessage>('mouseEnterMarkdownPreview', { cellId });
});
previewContainerNode.addEventListener('dragend', e => {
(e.target as HTMLElement).classList.remove('dragging');
});
previewContainerNode.addEventListener('mouseleave', () => {
postNotebookMessage<IMouseLeaveMarkdownPreviewMessage>('mouseLeaveMarkdownPreview', { cellId });
});
cellContainer.appendChild(previewContainerNode);
previewContainerNode.setAttribute('draggable', 'true');
const previewNode = document.createElement('div');
previewContainerNode.appendChild(previewNode);
previewContainerNode.addEventListener('dragstart', e => {
if (!e.dataTransfer) {
return;
}
e.dataTransfer.setData(markdownCellDragDataType, JSON.stringify({ cellId }));
// TODO: handle namespace
onDidCreateMarkdown.fire([undefined /* data.apiNamespace */, {
element: previewNode,
content: content
}]);
(e.target as HTMLElement).classList.add('dragging');
resizeObserve(previewContainerNode, `${cellId}_preview`, false);
postNotebookMessage<ICellDragStartMessage>('cell-drag-start', {
cellId: cellId,
position: { clientX: e.clientX, clientY: e.clientY },
});
});
previewContainerNode.addEventListener('drag', e => {
postNotebookMessage<ICellDragMessage>('cell-drag', {
cellId: cellId,
position: { clientX: e.clientX, clientY: e.clientY },
});
});
previewContainerNode.addEventListener('dragend', e => {
(e.target as HTMLElement).classList.remove('dragging');
});
cellContainer.appendChild(previewContainerNode);
const previewNode = document.createElement('div');
previewContainerNode.appendChild(previewNode);
// TODO: handle namespace
onDidCreateMarkdown.fire([undefined /* data.apiNamespace */, {
element: previewNode,
content: content
}]);
resizeObserve(previewContainerNode, `${cellId}_preview`, false);
postNotebookMessage<IDimensionMessage>('dimension', {
id: `${cellId}_preview`,
init: true,
data: {
height: previewContainerNode.clientHeight,
},
isOutput: false
});
} else {
updateMarkdownPreview(cellId, content);
}
postNotebookMessage<IDimensionMessage>('dimension', {
id: `${cellId}_preview`,
init: true,
data: {
height: previewContainerNode.clientHeight,
},
isOutput: false
});
}
function postNotebookMessage<T extends FromWebviewMessage>(
@ -795,21 +811,23 @@ function webviewPreloads() {
function updateMarkdownPreview(cellId: string, content: string) {
const previewContainerNode = document.getElementById(`${cellId}_preview`);
if (previewContainerNode) {
// TODO: handle namespace
onDidCreateMarkdown.fire([undefined /* data.apiNamespace */, {
element: previewContainerNode,
content: content
}]);
postNotebookMessage<IDimensionMessage>('dimension', {
id: `${cellId}_preview`,
data: {
height: previewContainerNode.clientHeight,
},
isOutput: false
});
if (!previewContainerNode) {
return;
}
// TODO: handle namespace
onDidCreateMarkdown.fire([undefined /* data.apiNamespace */, {
element: previewContainerNode,
content: content
}]);
postNotebookMessage<IDimensionMessage>('dimension', {
id: `${cellId}_preview`,
data: {
height: previewContainerNode.clientHeight,
},
isOutput: false
});
}
}

View file

@ -23,6 +23,8 @@ export class MarkdownCellViewModel extends BaseCellViewModel implements ICellVie
private _html: HTMLElement | null = null;
private _layoutInfo: MarkdownCellLayoutInfo;
private _version = 0;
get layoutInfo() {
return this._layoutInfo;
}
@ -81,6 +83,10 @@ export class MarkdownCellViewModel extends BaseCellViewModel implements ICellVie
this._onDidChangeState.fire({ cellIsHoveredChanged: true });
}
public get version(): number {
return this._version;
}
constructor(
readonly viewType: string,
readonly model: NotebookCellTextModel,
@ -158,7 +164,8 @@ export class MarkdownCellViewModel extends BaseCellViewModel implements ICellVie
restoreEditorViewState(editorViewStates: editorCommon.ICodeEditorViewState | null, totalHeight?: number) {
super.restoreEditorViewState(editorViewStates);
if (totalHeight !== undefined) {
// we might already warmup the viewport so the cell has a total height computed
if (totalHeight !== undefined && this._layoutInfo.totalHeight === 0) {
this._layoutInfo = {
fontInfo: this._layoutInfo.fontInfo,
editorWidth: this._layoutInfo.editorWidth,
@ -212,9 +219,14 @@ export class MarkdownCellViewModel extends BaseCellViewModel implements ICellVie
if (!this.textModel) {
const ref = await this.model.resolveTextModelRef();
this.textModel = ref.object.textEditorModel;
this._version = this.textModel.getVersionId();
this._register(ref);
this._register(this.textModel.onDidChangeContent(() => {
this._html = null;
if (this.textModel) {
this._version = this.textModel.getVersionId();
}
this._onDidChangeState.fire({ contentChanged: true });
}));
}

View file

@ -76,8 +76,13 @@ export class TestNotebookEditor implements INotebookEditor {
getCellsFromViewRange(startIndex: number, endIndex: number): ICellViewModel[] {
throw new Error('Method not implemented.');
}
getFocus(): ICellRange | undefined {
return undefined;
}
getVisibleRangesPlusViewportAboveBelow(): ICellRange[] {
throw new Error('Method not implemented.');
}
getSelection(): ICellRange | undefined {
throw new Error('Method not implemented.');
}
getSelections(): ICellRange[] {
@ -141,6 +146,7 @@ export class TestNotebookEditor implements INotebookEditor {
}
multipleKernelsAvailable: boolean = false;
onDidScroll: Event<void> = new Emitter<void>().event;
onDidChangeAvailableKernels: Event<void> = new Emitter<void>().event;
onDidChangeActiveCell: Event<void> = new Emitter<void>().event;
onDidChangeVisibleRanges: Event<void> = new Emitter<void>().event;

View file

@ -24,6 +24,7 @@ import { IHostService } from 'vs/workbench/services/host/browser/host';
import { isWeb } from 'vs/base/common/platform';
import { once } from 'vs/base/common/functional';
import { truncate } from 'vs/base/common/strings';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
export class RemoteStatusIndicator extends Disposable implements IWorkbenchContribution {
@ -53,7 +54,8 @@ export class RemoteStatusIndicator extends Disposable implements IWorkbenchContr
@IExtensionService private readonly extensionService: IExtensionService,
@IRemoteAgentService private readonly remoteAgentService: IRemoteAgentService,
@IRemoteAuthorityResolverService private readonly remoteAuthorityResolverService: IRemoteAuthorityResolverService,
@IHostService private readonly hostService: IHostService
@IHostService private readonly hostService: IHostService,
@IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService
) {
super();
@ -200,10 +202,11 @@ export class RemoteStatusIndicator extends Disposable implements IWorkbenchContr
const remoteIndicator = this.environmentService.options?.windowIndicator;
if (remoteIndicator) {
this.renderRemoteStatusIndicator(truncate(remoteIndicator.label, RemoteStatusIndicator.REMOTE_STATUS_LABEL_MAX_LENGTH), remoteIndicator.tooltip, remoteIndicator.command);
return;
}
// Remote Authority: show connection state
else if (this.remoteAuthority) {
if (this.remoteAuthority) {
const hostLabel = this.labelService.getHostLabel(Schemas.vscodeRemote, this.remoteAuthority) || this.remoteAuthority;
switch (this.connectionState) {
case 'initializing':
@ -218,13 +221,18 @@ export class RemoteStatusIndicator extends Disposable implements IWorkbenchContr
default:
this.renderRemoteStatusIndicator(`$(remote) ${truncate(hostLabel, RemoteStatusIndicator.REMOTE_STATUS_LABEL_MAX_LENGTH)}`, nls.localize('host.tooltip', "Editing on {0}", hostLabel));
}
return;
}
// workspace with label
const workspaceLabel = this.getWorkspaceLabel();
if (workspaceLabel) {
this.renderRemoteStatusIndicator(`$(remote) ${truncate(workspaceLabel, RemoteStatusIndicator.REMOTE_STATUS_LABEL_MAX_LENGTH)}`, nls.localize('workspace.tooltip', "Editing on {0}", workspaceLabel));
return;
}
// Remote Extensions Installed: offer the indicator to show actions
else if (this.remoteMenu.getActions().length > 0) {
if (this.remoteMenu.getActions().length > 0) {
this.renderRemoteStatusIndicator(`$(remote)`, nls.localize('noHost.tooltip', "Open a Remote Window"));
}
// No Remote Extensions: hide status indicator
else {
dispose(this.remoteStatusEntry);
@ -232,6 +240,15 @@ export class RemoteStatusIndicator extends Disposable implements IWorkbenchContr
}
}
private getWorkspaceLabel() {
const workspace = this.workspaceContextService.getWorkspace();
const loc = workspace.configuration || workspace.folders.length === 1 ? workspace.folders[0].uri : undefined;
if (loc) {
return this.labelService.getHostLabel(loc.scheme, loc.authority);
}
return undefined;
}
private renderRemoteStatusIndicator(text: string, tooltip?: string, command?: string, showProgress?: boolean): void {
const name = nls.localize('remoteHost', "Remote Host");
if (typeof command !== 'string' && this.remoteMenu.getActions().length > 0) {

View file

@ -184,10 +184,43 @@ Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration)
}
}
},
markdownDescription: localize('remote.portsAttributes', "Set default properties that are applied when a specific port number is forwarded. For example:\n\n```\n\"3000\": {\n \"label\": \"Labeled Port\"\n},\n\"40000-55000\": {\n \"onAutoForward\": \"ignore\"\n},\n\".+\\\\/server.js\": {\n \"onAutoForward\": \"openPreview\"\n}\n```"),
defaultSnippets: [{ body: { '${1:3000}': { label: '${2:My Port}', onAutoForward: 'openPreview' }, 'others': { onAutoForward: 'notify' } } }],
markdownDescription: localize('remote.portsAttributes', "Set properties that are applied when a specific port number is forwarded. For example:\n\n```\n\"3000\": {\n \"label\": \"Labeled Port\"\n},\n\"40000-55000\": {\n \"onAutoForward\": \"ignore\"\n},\n\".+\\\\/server.js\": {\n \"onAutoForward\": \"openPreview\"\n}\n```"),
defaultSnippets: [{ body: { '${1:3000}': { label: '${2:My Port}', onAutoForward: 'openPreview' } } }],
errorMessage: localize('remote.portsAttributes.patternError', "Must be a port number, range of port numbers, or regular expression."),
additionalProperties: false
},
'remote.portsAttributes.defaults': {
type: 'object',
properties: {
'onAutoForward': {
type: 'string',
enum: ['notify', 'openBrowser', 'openPreview', 'silent', 'ignore'],
enumDescriptions: [
localize('remote.portsAttributes.notify', "Shows a notification when a port is automatically forwarded."),
localize('remote.portsAttributes.openBrowser', "Opens the browser when the port is automatically forwarded. Depending on your settings, this could open an embedded browser."),
localize('remote.portsAttributes.openPreview', "Opens a preview in the same window when the port is automatically forwarded."),
localize('remote.portsAttributes.silent', "Shows no notification and takes no action when this port is automatically forwarded."),
localize('remote.portsAttributes.ignore', "This port will not be automatically forwarded.")
],
description: localize('remote.portsAttributes.onForward', "Defines the action that occurs when the port is discovered for automatic forwarding"),
default: 'notify'
},
'elevateIfNeeded': {
type: 'boolean',
description: localize('remote.portsAttributes.elevateIfNeeded', "Automatically prompt for elevation (if needed) when this port is forwarded. Elevate is required if the local port is a privileged port."),
default: false
},
'label': {
type: 'string',
description: localize('remote.portsAttributes.label', "Label that will be shown in the UI for this port."),
default: localize('remote.portsAttributes.labelDefault', "Labeled Port")
}
},
default: {
'onAutoForward': 'notify'
},
markdownDescription: localize('remote.portsAttributes.defaults', "Set default properties that are applied to all ports that don't get properties from the setting `remote.portsAttributes`. For example:\n\n```\n{\n \"onAutoForward\": \"notify\"\n}\n```"),
additionalProperties: false
}
}
});

View file

@ -715,7 +715,7 @@ export class SearchView extends ViewPane {
accessibilityProvider: this.treeAccessibilityProvider,
dnd: this.instantiationService.createInstance(SearchDND),
multipleSelectionSupport: false,
openOnFocus: true,
selectionNavigation: true,
overrideStyles: {
listBackground: this.getBackgroundColor()
}

View file

@ -76,6 +76,12 @@
outline-offset: 2px !important;
}
.workspace-trust-editor.settings-editor .workspace-trust-section-title {
font-weight: 600;
font-size: 18px;
padding-bottom: 5px;
}
/** Features List */
.workspace-trust-editor.settings-editor > .workspace-trust-features {
padding: 14px;
@ -83,12 +89,21 @@
user-select: text;
}
.workspace-trust-editor.settings-editor > .workspace-trust-features .workspace-trust-extensions-list {
padding-bottom: 10px;
}
.workspace-trust-editor.settings-editor > .workspace-trust-features .workspace-trust-extension-list-title {
font-size: 16px;
font-weight: 600;
line-height: 32px;
}
.workspace-trust-editor.settings-editor > .workspace-trust-features .workspace-trust-extension-list-description {
padding-bottom: 15px;
}
.workspace-trust-editor.settings-editor > .workspace-trust-features .workspace-trust-extension-list-entry {
display: flex;
margin-bottom: 10px;
@ -110,6 +125,10 @@
}
/** Settings */
.workspace-trust-editor.settings-editor .workspace-trust-settings .workspace-trust-section-title {
padding: 14px;
}
.workspace-trust-editor.settings-editor > .settings-body > .settings-tree-container .shadow.top {
left: initial;
margin-left: initial;

View file

@ -5,18 +5,17 @@
import { $, append, clearNode, Dimension, EventHelper } from 'vs/base/browser/dom';
import { ButtonBar } from 'vs/base/browser/ui/button/button';
import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement';
import * as arrays from 'vs/base/common/arrays';
import { CancellationToken } from 'vs/base/common/cancellation';
import { Codicon, registerCodicon } from 'vs/base/common/codicons';
import { Iterable } from 'vs/base/common/iterator';
import { DisposableStore } from 'vs/base/common/lifecycle';
import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle';
import { parseLinkedText } from 'vs/base/common/linkedText';
import { FileAccess } from 'vs/base/common/network';
import { joinPath } from 'vs/base/common/resources';
import { isArray } from 'vs/base/common/types';
import { URI } from 'vs/base/common/uri';
import { localize } from 'vs/nls';
import { DefaultIconPath } from 'vs/platform/extensionManagement/common/extensionManagement';
import { IExtensionDescription } from 'vs/platform/extensions/common/extensions';
import { ExtensionWorkspaceTrustRequirement } from 'vs/platform/extensions/common/extensions';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { Link } from 'vs/platform/opener/browser/link';
import { IStorageService } from 'vs/platform/storage/common/storage';
@ -27,9 +26,11 @@ import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace
import { WorkspaceTrustState } from 'vs/platform/workspace/common/workspaceTrust';
import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane';
import { EditorOptions, IEditorOpenContext } from 'vs/workbench/common/editor';
import { ExtensionsGridView, getExtensions } from 'vs/workbench/contrib/extensions/browser/extensionsViewer';
import { IExtension, IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions';
import { getInstalledExtensions, IExtensionStatus } from 'vs/workbench/contrib/extensions/common/extensionsUtils';
import { trustedForegroundColor, untrustedForegroundColor } from 'vs/workbench/contrib/workspace/browser/workspaceTrustColors';
import { IWorkspaceTrustSettingChangeEvent, WorkspaceTrustSettingArrayRenderer, WorkspaceTrustTree, WorkspaceTrustTreeModel } from 'vs/workbench/contrib/workspace/browser/workspaceTrustTree';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { WorkspaceTrustEditorInput } from 'vs/workbench/services/workspaces/browser/workspaceTrustEditorInput';
import { WorkspaceTrustEditorModel } from 'vs/workbench/services/workspaces/common/workspaceTrust';
@ -54,7 +55,6 @@ export class WorkspaceTrustEditor extends EditorPane {
private extensionsContainer!: HTMLElement;
private onDemandExtensionsContainer!: HTMLElement;
private onStartExtensionsContainer!: HTMLElement;
private extensionsRequiringTrust: IExtensionDescription[] = [];
// Settings Section
private configurationContainer!: HTMLElement;
@ -68,7 +68,7 @@ export class WorkspaceTrustEditor extends EditorPane {
@IThemeService themeService: IThemeService,
@IStorageService storageService: IStorageService,
@IWorkspaceContextService private readonly workspaceService: IWorkspaceContextService,
@IExtensionService private readonly extensionService: IExtensionService,
@IExtensionsWorkbenchService private readonly extensionWorkbenchService: IExtensionsWorkbenchService,
@IInstantiationService private readonly instantiationService: IInstantiationService
) { super(WorkspaceTrustEditor.ID, telemetryService, themeService, storageService); }
@ -95,17 +95,23 @@ export class WorkspaceTrustEditor extends EditorPane {
return;
}
this._register(model.dataModel.onDidChangeTrustState(() => {
this.render(model);
}));
this.extensionsRequiringTrust = (await this.extensionService.getExtensions()).filter(ext => ext.requiresWorkspaceTrust);
this.registerListeners(model);
this.render(model);
this.workspaceTrustEditorModel = model;
}
private registerListeners(model: WorkspaceTrustEditorModel): void {
this._register(model.dataModel.onDidChangeTrustState(() => {
this.render(model);
}));
this._register(this.extensionWorkbenchService.onChange(() => {
this.render(model);
}));
}
private getHeaderContainerClass(trustState: WorkspaceTrustState): string {
switch (trustState) {
case WorkspaceTrustState.Trusted:
@ -149,7 +155,7 @@ export class WorkspaceTrustEditor extends EditorPane {
}
private rerenderDisposables: DisposableStore = this._register(new DisposableStore());
private render(model: WorkspaceTrustEditorModel): void {
private async render(model: WorkspaceTrustEditorModel) {
this.rerenderDisposables.clear();
// Header Section
@ -202,50 +208,56 @@ export class WorkspaceTrustEditor extends EditorPane {
createButton(localize('doNotTrustButton', "Don't Trust"), model.currentWorkspaceTrustState !== WorkspaceTrustState.Untrusted, () => setTrustState(WorkspaceTrustState.Untrusted));
// Features List
const installedExtensions = await this.instantiationService.invokeFunction(getInstalledExtensions);
const onDemandExtensions = await this.getExtensionsByTrustRequirement(installedExtensions, 'onDemand');
const onStartExtensions = await this.getExtensionsByTrustRequirement(installedExtensions, 'onStart');
this.renderExtensionList(
localize('onStartExtensions', "Disabled Extensions"),
localize('onStartExtensionsDescription', "The following extensions require the workspace to be trusted. They will be disabled while the workspace is not trusted."),
this.onStartExtensionsContainer,
this.extensionsRequiringTrust.filter(ext => ext.requiresWorkspaceTrust === 'onStart'));
onStartExtensions);
this.renderExtensionList(
localize('onDemandExtensions', "Limited Extensions"),
localize('onDemandExtensionsDescription', "The following extensions can function partially in a non-trusted workspace. Some functionality will be turned off while the workspace is not trusted."),
this.onDemandExtensionsContainer,
this.extensionsRequiringTrust.filter(ext => ext.requiresWorkspaceTrust === 'onDemand'));
onDemandExtensions);
// Configuration Tree
this.workspaceTrustSettingsTreeModel.update(model.dataModel.getTrustStateInfo());
this.trustSettingsTree.setChildren(null, Iterable.map(this.workspaceTrustSettingsTreeModel.settings, s => { return { element: s }; }));
}
private renderExtensionList(title: string, parent: HTMLElement, extensions: IExtensionDescription[]) {
private async getExtensionsByTrustRequirement(extensions: IExtensionStatus[], trustRequirement: ExtensionWorkspaceTrustRequirement): Promise<IExtension[]> {
const filtered = extensions.filter(ext => ext.local.manifest.requiresWorkspaceTrust === trustRequirement);
const ids = filtered.map(ext => ext.identifier.id);
return getExtensions(ids, this.extensionWorkbenchService);
}
private renderExtensionList(title: string, description: string, parent: HTMLElement, extensions: IExtension[]) {
clearNode(parent);
if (!extensions.length) {
return;
}
const titleElement = append(parent, $('.workspace-trust-extension-list-title'));
titleElement.innerText = title;
const listContainer = append(parent, $('.workspace-trust-extension-list'));
extensions.forEach(ext => {
const extensionEntry = append(listContainer, $('.workspace-trust-extension-list-entry'));
this.renderExtension(extensionEntry, ext);
});
}
const descriptionElement = append(parent, $('.workspace-trust-extension-list-description'));
descriptionElement.innerText = description;
private renderExtension(parent: HTMLElement, extension: IExtensionDescription) {
const iconContainer = append(parent, $('.workspace-trust-extension-icon'));
const icon = append(iconContainer, $<HTMLImageElement>('img.icon'));
const textContainer = append(parent, $('.workspace-trust-extension-text'));
const nameContainer = append(textContainer, $('.workspace-trust-extension-name'));
const content = $('div', { class: 'subcontent' });
const scrollableContent = new DomScrollableElement(content, { useShadows: false });
append(parent, scrollableContent.getDomNode());
const extensionDescription = append(textContainer, $('.workspace-trust-extension-description'));
const extensionsGridView = this.instantiationService.createInstance(ExtensionsGridView, content);
extensionsGridView.setExtensions(extensions);
scrollableContent.scanDomNode();
nameContainer.innerText = extension.displayName || extension.name;
icon.src = extension.icon
? FileAccess.asBrowserUri(joinPath(extension.extensionLocation, extension.icon)).toString(true)
: DefaultIconPath;
extensionDescription.innerText = extension.description || '';
this.rerenderDisposables.add(scrollableContent);
this.rerenderDisposables.add(extensionsGridView);
this.rerenderDisposables.add(toDisposable(arrays.insert(this.layoutParticipants, { layout: () => scrollableContent.scanDomNode() })));
}
private createHeaderElement(parent: HTMLElement): void {
@ -262,6 +274,9 @@ export class WorkspaceTrustEditor extends EditorPane {
private createConfigurationElement(parent: HTMLElement): void {
this.configurationContainer = append(parent, $('.workspace-trust-settings.settings-body'));
const titleContainer = append(this.configurationContainer, $('.workspace-trust-section-title'));
titleContainer.innerText = localize('configurationSectionTitle', "Configure All Workspaces");
const workspaceTrustTreeContainer = append(this.configurationContainer, $('.workspace-trust-settings-tree-container.settings-tree-container'));
const renderer = this.instantiationService.createInstance(WorkspaceTrustSettingArrayRenderer,);
@ -276,9 +291,12 @@ export class WorkspaceTrustEditor extends EditorPane {
private createAffectedFeaturesElement(parent: HTMLElement): void {
this.affectedFeaturesContainer = append(parent, $('.workspace-trust-features'));
const titleContainer = append(this.affectedFeaturesContainer, $('.workspace-trust-section-title'));
titleContainer.innerText = localize('affectedFeaturesTitle', "Features Affected By Workspace Trust");
this.extensionsContainer = append(this.affectedFeaturesContainer, $('.workspace-trust-extensions'));
this.onDemandExtensionsContainer = append(this.extensionsContainer, $('.workspace-trust-extensions-on-demand'));
this.onStartExtensionsContainer = append(this.extensionsContainer, $('.workspace-trust-extensions-on-start'));
this.onDemandExtensionsContainer = append(this.extensionsContainer, $('.workspace-trust-extensions-list'));
this.onStartExtensionsContainer = append(this.extensionsContainer, $('.workspace-trust-extensions-list'));
}
private onDidChangeSetting(change: IWorkspaceTrustSettingChangeEvent) {
@ -295,6 +313,7 @@ export class WorkspaceTrustEditor extends EditorPane {
}
}
private layoutParticipants: { layout: () => void; }[] = [];
layout(dimension: Dimension): void {
if (!this.isVisible()) {
return;
@ -304,5 +323,9 @@ export class WorkspaceTrustEditor extends EditorPane {
this.configurationContainer.style.height = `${listHeight}`;
this.trustSettingsTree.layout(listHeight, dimension.width);
this.layoutParticipants.forEach(participant => {
participant.layout();
});
}
}

View file

@ -5,346 +5,50 @@
import * as fs from 'fs';
import { gracefulify } from 'graceful-fs';
import { zoomLevelToZoomFactor } from 'vs/platform/windows/common/windows';
import { mark } from 'vs/base/common/performance';
import { Workbench } from 'vs/workbench/browser/workbench';
import { NativeWindow } from 'vs/workbench/electron-sandbox/window';
import { setZoomLevel, setZoomFactor, setFullscreen } from 'vs/base/browser/browser';
import { domContentLoaded } from 'vs/base/browser/dom';
import { onUnexpectedError } from 'vs/base/common/errors';
import { URI } from 'vs/base/common/uri';
import { WorkspaceService } from 'vs/workbench/services/configuration/browser/configurationService';
import { NativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-browser/environmentService';
import { INativeWorkbenchConfiguration, INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-sandbox/environmentService';
import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection';
import { isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier, IWorkspaceInitializationPayload, reviveIdentifier } from 'vs/platform/workspaces/common/workspaces';
import { ILoggerService, ILogService } from 'vs/platform/log/common/log';
import { NativeStorageService } from 'vs/platform/storage/electron-sandbox/storageService';
import { INativeWorkbenchConfiguration } from 'vs/workbench/services/environment/electron-sandbox/environmentService';
import { ILogService } from 'vs/platform/log/common/log';
import { Schemas } from 'vs/base/common/network';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { IWorkbenchConfigurationService } from 'vs/workbench/services/configuration/common/configuration';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { Disposable } from 'vs/base/common/lifecycle';
import { registerWindowDriver } from 'vs/platform/driver/electron-browser/driver';
import { IMainProcessService } from 'vs/platform/ipc/electron-sandbox/services';
import { RemoteAuthorityResolverService } from 'vs/platform/remote/electron-sandbox/remoteAuthorityResolverService';
import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remoteAuthorityResolver';
import { RemoteAgentService } from 'vs/workbench/services/remote/electron-sandbox/remoteAgentServiceImpl';
import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService';
import { FileService } from 'vs/platform/files/common/fileService';
import { IFileService } from 'vs/platform/files/common/files';
import { DiskFileSystemProvider } from 'vs/platform/files/electron-browser/diskFileSystemProvider';
import { RemoteFileSystemProvider } from 'vs/workbench/services/remote/common/remoteAgentFileSystemChannel';
import { ConfigurationCache } from 'vs/workbench/services/configuration/electron-sandbox/configurationCache';
import { ISignService } from 'vs/platform/sign/common/sign';
import { FileUserDataProvider } from 'vs/workbench/services/userData/common/fileUserDataProvider';
import { basename } from 'vs/base/common/path';
import { IProductService } from 'vs/platform/product/common/productService';
import product from 'vs/platform/product/common/product';
import { NativeLogService } from 'vs/workbench/services/log/electron-sandbox/logService';
import { INativeHostService } from 'vs/platform/native/electron-sandbox/native';
import { NativeHostService } from 'vs/platform/native/electron-sandbox/nativeHostService';
import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity';
import { UriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentityService';
import { KeyboardLayoutService } from 'vs/workbench/services/keybinding/electron-sandbox/nativeKeyboardLayout';
import { IKeyboardLayoutService } from 'vs/platform/keyboardLayout/common/keyboardLayout';
import { ElectronIPCMainProcessService } from 'vs/platform/ipc/electron-sandbox/mainProcessService';
import { LoggerChannelClient } from 'vs/platform/log/common/logIpc';
import { ProxyChannel } from 'vs/base/parts/ipc/common/ipc';
import { productService, SharedDesktopMain } from 'vs/workbench/electron-sandbox/shared.desktop.main';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { registerWindowDriver } from 'vs/platform/driver/electron-browser/driver';
class DesktopMain extends Disposable {
class DesktopMain extends SharedDesktopMain {
private readonly productService: IProductService = { _serviceBrand: undefined, ...product };
private readonly environmentService = new NativeWorkbenchEnvironmentService(this.configuration, this.productService);
constructor(private configuration: INativeWorkbenchConfiguration) {
super();
this.init();
}
private init(): void {
constructor(configuration: INativeWorkbenchConfiguration) {
super(configuration, new NativeWorkbenchEnvironmentService(configuration, productService));
// Enable gracefulFs
gracefulify(fs);
// Massage configuration file URIs
this.reviveUris();
// Browser config
const zoomLevel = this.configuration.zoomLevel || 0;
setZoomFactor(zoomLevelToZoomFactor(zoomLevel));
setZoomLevel(zoomLevel, true /* isTrusted */);
setFullscreen(!!this.configuration.fullscreen);
}
private reviveUris() {
// Workspace
const workspace = reviveIdentifier(this.configuration.workspace);
if (isWorkspaceIdentifier(workspace) || isSingleFolderWorkspaceIdentifier(workspace)) {
this.configuration.workspace = workspace;
}
// Files
const filesToWait = this.configuration.filesToWait;
const filesToWaitPaths = filesToWait?.paths;
[filesToWaitPaths, this.configuration.filesToOpenOrCreate, this.configuration.filesToDiff].forEach(paths => {
if (Array.isArray(paths)) {
paths.forEach(path => {
if (path.fileUri) {
path.fileUri = URI.revive(path.fileUri);
}
});
}
});
if (filesToWait) {
filesToWait.waitMarkerFileUri = URI.revive(filesToWait.waitMarkerFileUri);
}
}
async open(): Promise<void> {
const services = await this.initServices();
await domContentLoaded();
mark('code/willStartWorkbench');
// Create Workbench
const workbench = new Workbench(document.body, services.serviceCollection, services.logService);
// Listeners
this.registerListeners(workbench, services.storageService);
// Startup
const instantiationService = workbench.startup();
// Window
this._register(instantiationService.createInstance(NativeWindow));
protected joinOpen(instantiationService: IInstantiationService): void {
// Driver
if (this.configuration.driver) {
instantiationService.invokeFunction(async accessor => this._register(await registerWindowDriver(accessor, this.configuration.windowId)));
}
// Logging
services.logService.trace('workbench configuration', JSON.stringify(this.configuration));
}
private registerListeners(workbench: Workbench, storageService: NativeStorageService): void {
// Workbench Lifecycle
this._register(workbench.onWillShutdown(event => event.join(storageService.close(), 'join.closeStorage')));
this._register(workbench.onShutdown(() => this.dispose()));
protected createLogService(loggerService: LoggerChannelClient, mainProcessService: IMainProcessService): ILogService {
return this._register(new NativeLogService(`renderer${this.configuration.windowId}`, loggerService, mainProcessService, this.environmentService));
}
private async initServices(): Promise<{ serviceCollection: ServiceCollection, logService: ILogService, storageService: NativeStorageService }> {
const serviceCollection = new ServiceCollection();
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
//
// NOTE: Please do NOT register services here. Use `registerSingleton()`
// from `workbench.common.main.ts` if the service is shared between
// desktop and web or `workbench.sandbox.main.ts` if the service
// is desktop only.
//
// DO NOT add services to `workbench.desktop.main.ts`, always add
// to `workbench.sandbox.main.ts` to support our Electron sandbox
//
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
// Main Process
const mainProcessService = this._register(new ElectronIPCMainProcessService(this.configuration.windowId));
serviceCollection.set(IMainProcessService, mainProcessService);
// Environment
serviceCollection.set(INativeWorkbenchEnvironmentService, this.environmentService);
// Product
serviceCollection.set(IProductService, this.productService);
// Logger
const loggerService = new LoggerChannelClient(mainProcessService.getChannel('logger'));
serviceCollection.set(ILoggerService, loggerService);
// Log
const logService = this._register(new NativeLogService(`renderer${this.configuration.windowId}`, loggerService, mainProcessService, this.environmentService));
serviceCollection.set(ILogService, logService);
// Remote
const remoteAuthorityResolverService = new RemoteAuthorityResolverService();
serviceCollection.set(IRemoteAuthorityResolverService, remoteAuthorityResolverService);
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
//
// NOTE: Please do NOT register services here. Use `registerSingleton()`
// from `workbench.common.main.ts` if the service is shared between
// desktop and web or `workbench.sandbox.main.ts` if the service
// is desktop only.
//
// DO NOT add services to `workbench.desktop.main.ts`, always add
// to `workbench.sandbox.main.ts` to support our Electron sandbox
//
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
// Sign
const signService = ProxyChannel.toService<ISignService>(mainProcessService.getChannel('sign'));
serviceCollection.set(ISignService, signService);
// Remote Agent
const remoteAgentService = this._register(new RemoteAgentService(this.environmentService, this.productService, remoteAuthorityResolverService, signService, logService));
serviceCollection.set(IRemoteAgentService, remoteAgentService);
// Native Host
const nativeHostService = new NativeHostService(this.configuration.windowId, mainProcessService) as INativeHostService;
serviceCollection.set(INativeHostService, nativeHostService);
// Files
const fileService = this._register(new FileService(logService));
serviceCollection.set(IFileService, fileService);
protected registerFileSystemProviders(fileService: IFileService, logService: ILogService, nativeHostService: INativeHostService): void {
// Local Files
const diskFileSystemProvider = this._register(new DiskFileSystemProvider(logService, nativeHostService));
fileService.registerProvider(Schemas.file, diskFileSystemProvider);
// User Data Provider
fileService.registerProvider(Schemas.userData, new FileUserDataProvider(Schemas.file, diskFileSystemProvider, Schemas.userData, logService));
// Uri Identity
const uriIdentityService = new UriIdentityService(fileService);
serviceCollection.set(IUriIdentityService, uriIdentityService);
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
//
// NOTE: Please do NOT register services here. Use `registerSingleton()`
// from `workbench.common.main.ts` if the service is shared between
// desktop and web or `workbench.sandbox.main.ts` if the service
// is desktop only.
//
// DO NOT add services to `workbench.desktop.main.ts`, always add
// to `workbench.sandbox.main.ts` to support our Electron sandbox
//
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
const connection = remoteAgentService.getConnection();
if (connection) {
const remoteFileSystemProvider = this._register(new RemoteFileSystemProvider(remoteAgentService));
fileService.registerProvider(Schemas.vscodeRemote, remoteFileSystemProvider);
}
const payload = this.resolveWorkspaceInitializationPayload();
const services = await Promise.all([
this.createWorkspaceService(payload, fileService, remoteAgentService, uriIdentityService, logService).then(service => {
// Workspace
serviceCollection.set(IWorkspaceContextService, service);
// Configuration
serviceCollection.set(IWorkbenchConfigurationService, service);
return service;
}),
this.createStorageService(payload, mainProcessService).then(service => {
// Storage
serviceCollection.set(IStorageService, service);
return service;
}),
this.createKeyboardLayoutService(mainProcessService).then(service => {
// KeyboardLayout
serviceCollection.set(IKeyboardLayoutService, service);
return service;
})
]);
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
//
// NOTE: Please do NOT register services here. Use `registerSingleton()`
// from `workbench.common.main.ts` if the service is shared between
// desktop and web or `workbench.sandbox.main.ts` if the service
// is desktop only.
//
// DO NOT add services to `workbench.desktop.main.ts`, always add
// to `workbench.sandbox.main.ts` to support our Electron sandbox
//
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
return { serviceCollection, logService, storageService: services[1] };
}
private resolveWorkspaceInitializationPayload(): IWorkspaceInitializationPayload {
let workspaceInitializationPayload: IWorkspaceInitializationPayload | undefined = this.configuration.workspace;
// Fallback to empty workspace if we have no payload yet.
if (!workspaceInitializationPayload) {
let id: string;
if (this.configuration.backupPath) {
id = basename(this.configuration.backupPath); // we know the backupPath must be a unique path so we leverage its name as workspace ID
} else if (this.environmentService.isExtensionDevelopment) {
id = 'ext-dev'; // extension development window never stores backups and is a singleton
} else {
throw new Error('Unexpected window configuration without backupPath');
}
workspaceInitializationPayload = { id };
}
return workspaceInitializationPayload;
}
private async createWorkspaceService(payload: IWorkspaceInitializationPayload, fileService: FileService, remoteAgentService: IRemoteAgentService, uriIdentityService: IUriIdentityService, logService: ILogService): Promise<WorkspaceService> {
const workspaceService = new WorkspaceService({ remoteAuthority: this.environmentService.remoteAuthority, configurationCache: new ConfigurationCache(URI.file(this.environmentService.userDataPath), fileService) }, this.environmentService, fileService, remoteAgentService, uriIdentityService, logService);
try {
await workspaceService.initialize(payload);
return workspaceService;
} catch (error) {
onUnexpectedError(error);
return workspaceService;
}
}
private async createStorageService(payload: IWorkspaceInitializationPayload, mainProcessService: IMainProcessService): Promise<NativeStorageService> {
const storageService = new NativeStorageService(payload, mainProcessService, this.environmentService);
try {
await storageService.initialize();
return storageService;
} catch (error) {
onUnexpectedError(error);
return storageService;
}
}
private async createKeyboardLayoutService(mainProcessService: IMainProcessService): Promise<KeyboardLayoutService> {
const keyboardLayoutService = new KeyboardLayoutService(mainProcessService);
try {
await keyboardLayoutService.initialize();
return keyboardLayoutService;
} catch (error) {
onUnexpectedError(error);
return keyboardLayoutService;
}
}
}

View file

@ -3,322 +3,34 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { zoomLevelToZoomFactor } from 'vs/platform/windows/common/windows';
import { mark } from 'vs/base/common/performance';
import { Workbench } from 'vs/workbench/browser/workbench';
import { NativeWindow } from 'vs/workbench/electron-sandbox/window';
import { setZoomLevel, setZoomFactor, setFullscreen } from 'vs/base/browser/browser';
import { domContentLoaded } from 'vs/base/browser/dom';
import { onUnexpectedError } from 'vs/base/common/errors';
import { URI } from 'vs/base/common/uri';
import { INativeWorkbenchConfiguration, INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-sandbox/environmentService';
import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection';
import { isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier, IWorkspaceInitializationPayload, reviveIdentifier } from 'vs/platform/workspaces/common/workspaces';
import { ILoggerService, ILogService } from 'vs/platform/log/common/log';
import { NativeStorageService } from 'vs/platform/storage/electron-sandbox/storageService';
import { INativeWorkbenchConfiguration } from 'vs/workbench/services/environment/electron-sandbox/environmentService';
import { ILogService } from 'vs/platform/log/common/log';
import { Schemas } from 'vs/base/common/network';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { IWorkbenchConfigurationService } from 'vs/workbench/services/configuration/common/configuration';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { Disposable } from 'vs/base/common/lifecycle';
import { IMainProcessService } from 'vs/platform/ipc/electron-sandbox/services';
import { RemoteAuthorityResolverService } from 'vs/platform/remote/electron-sandbox/remoteAuthorityResolverService';
import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remoteAuthorityResolver';
import { RemoteAgentService } from 'vs/workbench/services/remote/electron-sandbox/remoteAgentServiceImpl';
import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService';
import { FileService } from 'vs/platform/files/common/fileService';
import { IFileService } from 'vs/platform/files/common/files';
import { RemoteFileSystemProvider } from 'vs/workbench/services/remote/common/remoteAgentFileSystemChannel';
import { ISignService } from 'vs/platform/sign/common/sign';
import { FileUserDataProvider } from 'vs/workbench/services/userData/common/fileUserDataProvider';
import { basename } from 'vs/base/common/path';
import { IProductService } from 'vs/platform/product/common/productService';
import product from 'vs/platform/product/common/product';
import { INativeHostService } from 'vs/platform/native/electron-sandbox/native';
import { NativeHostService } from 'vs/platform/native/electron-sandbox/nativeHostService';
import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity';
import { UriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentityService';
import { KeyboardLayoutService } from 'vs/workbench/services/keybinding/electron-sandbox/nativeKeyboardLayout';
import { IKeyboardLayoutService } from 'vs/platform/keyboardLayout/common/keyboardLayout';
import { ElectronIPCMainProcessService } from 'vs/platform/ipc/electron-sandbox/mainProcessService';
import { SimpleConfigurationService, simpleFileSystemProvider, SimpleNativeWorkbenchEnvironmentService, SimpleWorkspaceService, SimpleLogService } from 'vs/workbench/electron-sandbox/sandbox.simpleservices';
import { simpleFileSystemProvider, SimpleNativeWorkbenchEnvironmentService, SimpleLogService, simpleWorkspace } from 'vs/workbench/electron-sandbox/sandbox.simpleservices';
import { LoggerChannelClient } from 'vs/platform/log/common/logIpc';
import { ProxyChannel } from 'vs/base/parts/ipc/common/ipc';
import { SharedDesktopMain } from 'vs/workbench/electron-sandbox/shared.desktop.main';
class DesktopMain extends Disposable {
class DesktopMain extends SharedDesktopMain {
private readonly productService: IProductService = { _serviceBrand: undefined, ...product };
private readonly environmentService = new SimpleNativeWorkbenchEnvironmentService(this.configuration);
constructor(private configuration: INativeWorkbenchConfiguration) {
super();
this.init();
constructor(configuration: INativeWorkbenchConfiguration) {
super({ ...configuration, workspace: simpleWorkspace }, new SimpleNativeWorkbenchEnvironmentService(configuration));
}
private init(): void {
// Massage configuration file URIs
this.reviveUris();
// Browser config
const zoomLevel = this.configuration.zoomLevel || 0;
setZoomFactor(zoomLevelToZoomFactor(zoomLevel));
setZoomLevel(zoomLevel, true /* isTrusted */);
setFullscreen(!!this.configuration.fullscreen);
protected createLogService(loggerService: LoggerChannelClient, mainProcessService: IMainProcessService): ILogService {
return new SimpleLogService(); // we can only use the real logger, once `IEnvironmentService#logFile` has a proper file:// based value (https://github.com/microsoft/vscode/issues/116829)
}
private reviveUris() {
// Workspace
const workspace = reviveIdentifier(this.configuration.workspace);
if (isWorkspaceIdentifier(workspace) || isSingleFolderWorkspaceIdentifier(workspace)) {
this.configuration.workspace = workspace;
}
// Files
const filesToWait = this.configuration.filesToWait;
const filesToWaitPaths = filesToWait?.paths;
[filesToWaitPaths, this.configuration.filesToOpenOrCreate, this.configuration.filesToDiff].forEach(paths => {
if (Array.isArray(paths)) {
paths.forEach(path => {
if (path.fileUri) {
path.fileUri = URI.revive(path.fileUri);
}
});
}
});
if (filesToWait) {
filesToWait.waitMarkerFileUri = URI.revive(filesToWait.waitMarkerFileUri);
}
}
async open(): Promise<void> {
const services = await this.initServices();
await domContentLoaded();
mark('code/willStartWorkbench');
// Create Workbench
const workbench = new Workbench(document.body, services.serviceCollection, services.logService);
// Listeners
this.registerListeners(workbench, services.storageService);
// Startup
const instantiationService = workbench.startup();
// Window
this._register(instantiationService.createInstance(NativeWindow));
// Logging
services.logService.trace('workbench configuration', JSON.stringify(this.configuration));
}
private registerListeners(workbench: Workbench, storageService: NativeStorageService): void {
// Workbench Lifecycle
this._register(workbench.onWillShutdown(event => event.join(storageService.close(), 'join.closeStorage')));
this._register(workbench.onShutdown(() => this.dispose()));
}
private async initServices(): Promise<{ serviceCollection: ServiceCollection, logService: ILogService, storageService: NativeStorageService }> {
const serviceCollection = new ServiceCollection();
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
//
// NOTE: Please do NOT register services here. Use `registerSingleton()`
// from `workbench.common.main.ts` if the service is shared between
// desktop and web or `workbench.sandbox.main.ts` if the service
// is desktop only.
//
// DO NOT add services to `workbench.desktop.main.ts`, always add
// to `workbench.sandbox.main.ts` to support our Electron sandbox
//
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
// Main Process
const mainProcessService = this._register(new ElectronIPCMainProcessService(this.configuration.windowId));
serviceCollection.set(IMainProcessService, mainProcessService);
// Environment
serviceCollection.set(INativeWorkbenchEnvironmentService, this.environmentService);
// Product
serviceCollection.set(IProductService, this.productService);
// Logger
const loggerService = new LoggerChannelClient(mainProcessService.getChannel('logger'));
serviceCollection.set(ILoggerService, loggerService);
// Log (we can only use the real logger, once `IEnvironmentService#logFile` has a proper file:// based value (https://github.com/microsoft/vscode/issues/116829))
const logService = new SimpleLogService();
serviceCollection.set(ILogService, logService);
// Remote
const remoteAuthorityResolverService = new RemoteAuthorityResolverService();
serviceCollection.set(IRemoteAuthorityResolverService, remoteAuthorityResolverService);
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
//
// NOTE: Please do NOT register services here. Use `registerSingleton()`
// from `workbench.common.main.ts` if the service is shared between
// desktop and web or `workbench.sandbox.main.ts` if the service
// is desktop only.
//
// DO NOT add services to `workbench.desktop.main.ts`, always add
// to `workbench.sandbox.main.ts` to support our Electron sandbox
//
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
// Sign
const signService = ProxyChannel.toService<ISignService>(mainProcessService.getChannel('sign'));
serviceCollection.set(ISignService, signService);
// Remote Agent
const remoteAgentService = this._register(new RemoteAgentService(this.environmentService, this.productService, remoteAuthorityResolverService, signService, logService));
serviceCollection.set(IRemoteAgentService, remoteAgentService);
// Native Host
const nativeHostService = new NativeHostService(this.configuration.windowId, mainProcessService) as INativeHostService;
serviceCollection.set(INativeHostService, nativeHostService);
// Files
const fileService = this._register(new FileService(logService));
serviceCollection.set(IFileService, fileService);
protected registerFileSystemProviders(fileService: IFileService, logService: ILogService, nativeHostService: INativeHostService): void {
// Local Files
fileService.registerProvider(Schemas.file, simpleFileSystemProvider);
// User Data Provider
fileService.registerProvider(Schemas.userData, new FileUserDataProvider(Schemas.file, simpleFileSystemProvider, Schemas.userData, logService));
// Uri Identity
const uriIdentityService = new UriIdentityService(fileService);
serviceCollection.set(IUriIdentityService, uriIdentityService);
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
//
// NOTE: Please do NOT register services here. Use `registerSingleton()`
// from `workbench.common.main.ts` if the service is shared between
// desktop and web or `workbench.sandbox.main.ts` if the service
// is desktop only.
//
// DO NOT add services to `workbench.desktop.main.ts`, always add
// to `workbench.sandbox.main.ts` to support our Electron sandbox
//
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
const connection = remoteAgentService.getConnection();
if (connection) {
const remoteFileSystemProvider = this._register(new RemoteFileSystemProvider(remoteAgentService));
fileService.registerProvider(Schemas.vscodeRemote, remoteFileSystemProvider);
}
const payload = this.resolveWorkspaceInitializationPayload();
const services = await Promise.all([
this.createWorkspaceService().then(service => {
// Workspace
serviceCollection.set(IWorkspaceContextService, service);
// Configuration
serviceCollection.set(IWorkbenchConfigurationService, new SimpleConfigurationService());
return service;
}),
this.createStorageService(payload, mainProcessService).then(service => {
// Storage
serviceCollection.set(IStorageService, service);
return service;
}),
this.createKeyboardLayoutService(mainProcessService).then(service => {
// KeyboardLayout
serviceCollection.set(IKeyboardLayoutService, service);
return service;
})
]);
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
//
// NOTE: Please do NOT register services here. Use `registerSingleton()`
// from `workbench.common.main.ts` if the service is shared between
// desktop and web or `workbench.sandbox.main.ts` if the service
// is desktop only.
//
// DO NOT add services to `workbench.desktop.main.ts`, always add
// to `workbench.sandbox.main.ts` to support our Electron sandbox
//
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
return { serviceCollection, logService, storageService: services[1] };
}
private resolveWorkspaceInitializationPayload(): IWorkspaceInitializationPayload {
let workspaceInitializationPayload: IWorkspaceInitializationPayload | undefined = this.configuration.workspace;
// Fallback to empty workspace if we have no payload yet.
if (!workspaceInitializationPayload) {
let id: string;
if (this.configuration.backupPath) {
id = basename(this.configuration.backupPath); // we know the backupPath must be a unique path so we leverage its name as workspace ID
} else if (this.environmentService.isExtensionDevelopment) {
id = 'ext-dev'; // extension development window never stores backups and is a singleton
} else {
throw new Error('Unexpected window configuration without backupPath');
}
workspaceInitializationPayload = { id };
}
return workspaceInitializationPayload;
}
private async createWorkspaceService(): Promise<IWorkspaceContextService> {
return new SimpleWorkspaceService();
}
private async createStorageService(payload: IWorkspaceInitializationPayload, mainProcessService: IMainProcessService): Promise<NativeStorageService> {
const storageService = new NativeStorageService(payload, mainProcessService, this.environmentService);
try {
await storageService.initialize();
return storageService;
} catch (error) {
onUnexpectedError(error);
return storageService;
}
}
private async createKeyboardLayoutService(mainProcessService: IMainProcessService): Promise<KeyboardLayoutService> {
const keyboardLayoutService = new KeyboardLayoutService(mainProcessService);
try {
await keyboardLayoutService.initialize();
return keyboardLayoutService;
} catch (error) {
onUnexpectedError(error);
return keyboardLayoutService;
}
}
}

View file

@ -10,20 +10,18 @@ import { URI } from 'vs/base/common/uri';
import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFilesystemProvider';
import { Event } from 'vs/base/common/event';
import { IAddressProvider } from 'vs/platform/remote/common/remoteAgentConnection';
import { SimpleConfigurationService as BaseSimpleConfigurationService } from 'vs/editor/standalone/browser/simpleServices';
import { ExtensionKind } from 'vs/platform/extensions/common/extensions';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { IExtensionService, NullExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { isWindows } from 'vs/base/common/platform';
import { IWebviewService, WebviewContentOptions, WebviewElement, WebviewExtensionDescription, WebviewOptions, WebviewOverlay } from 'vs/workbench/contrib/webview/browser/webview';
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
import { AbstractTextFileService } from 'vs/workbench/services/textfile/browser/textFileService';
import { ITunnelProvider, ITunnelService, RemoteTunnel, TunnelProviderFeatures } from 'vs/platform/remote/common/tunnel';
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
import { ISingleFolderWorkspaceIdentifier, IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces';
import { ISingleFolderWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces';
import { ITaskProvider, ITaskService, ITaskSummary, ProblemMatcherRunOptions, Task, TaskFilter, TaskTerminateResponse, WorkspaceFolderTaskResult } from 'vs/workbench/contrib/tasks/common/taskService';
import { Action } from 'vs/base/common/actions';
import { LinkedMap } from 'vs/base/common/map';
import { IWorkspace, IWorkspaceContextService, IWorkspaceFolder, WorkbenchState, WorkspaceFolder } from 'vs/platform/workspace/common/workspace';
import { IWorkspace, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace';
import { CustomTask, ContributedTask, InMemoryTask, TaskRunSource, ConfiguringTask, TaskIdentifier, TaskSorter } from 'vs/workbench/contrib/tasks/common/tasks';
import { TaskSystemInfo } from 'vs/workbench/contrib/tasks/common/taskSystem';
import { joinPath } from 'vs/base/common/resources';
@ -35,12 +33,12 @@ import type { IWorkbenchConstructionOptions } from 'vs/workbench/workbench.web.a
import { Schemas } from 'vs/base/common/network';
import { TerminalInstanceService } from 'vs/workbench/contrib/terminal/browser/terminalInstanceService';
import { ITerminalInstanceService } from 'vs/workbench/contrib/terminal/browser/terminal';
import { IWorkbenchConfigurationService } from 'vs/workbench/services/configuration/common/configuration';
import { ConsoleLogger, LogService } from 'vs/platform/log/common/log';
//#region Environment
const userDataDir = URI.file('/sandbox-user-data-dir');
export class SimpleNativeWorkbenchEnvironmentService implements INativeWorkbenchEnvironmentService {
declare readonly _serviceBrand: undefined;
@ -49,7 +47,7 @@ export class SimpleNativeWorkbenchEnvironmentService implements INativeWorkbench
readonly configuration: INativeWorkbenchConfiguration
) { }
get userRoamingDataHome(): URI { return URI.file('/sandbox-user-data-dir').with({ scheme: Schemas.userData }); }
get userRoamingDataHome(): URI { return userDataDir.with({ scheme: Schemas.userData }); }
get settingsResource(): URI { return joinPath(this.userRoamingDataHome, 'settings.json'); }
get argvResource(): URI { return joinPath(this.userRoamingDataHome, 'argv.json'); }
get snippetsHome(): URI { return joinPath(this.userRoamingDataHome, 'snippets'); }
@ -61,6 +59,7 @@ export class SimpleNativeWorkbenchEnvironmentService implements INativeWorkbench
get serviceMachineIdResource(): URI { return joinPath(this.userRoamingDataHome, 'machineid'); }
get userDataSyncLogResource(): URI { return joinPath(this.userRoamingDataHome, 'syncLog'); }
get userDataSyncHome(): URI { return joinPath(this.userRoamingDataHome, 'syncHome'); }
get userDataPath(): string { return userDataDir.fsPath; }
get tmpDir(): URI { return joinPath(this.userRoamingDataHome, 'tmp'); }
get logsPath(): string { return joinPath(this.userRoamingDataHome, 'logs').path; }
@ -83,6 +82,7 @@ export class SimpleNativeWorkbenchEnvironmentService implements INativeWorkbench
isExtensionDevelopment: boolean = false;
disableExtensions: boolean | string[] = [];
extensionDevelopmentLocationURI?: URI[] | undefined;
extensionDevelopmentKind?: ExtensionKind[] | undefined;
extensionTestsLocationURI?: URI | undefined;
logLevel?: string | undefined;
@ -92,7 +92,6 @@ export class SimpleNativeWorkbenchEnvironmentService implements INativeWorkbench
appRoot: string = undefined!;
userHome: URI = undefined!;
appSettingsHome: URI = undefined!;
userDataPath: string = undefined!;
machineSettingsResource: URI = undefined!;
log?: string | undefined;
@ -123,49 +122,7 @@ export class SimpleNativeWorkbenchEnvironmentService implements INativeWorkbench
//#region Workspace
export const workspaceResource = URI.file(isWindows ? '\\simpleWorkspace' : '/simpleWorkspace');
export class SimpleWorkspaceService implements IWorkspaceContextService {
declare readonly _serviceBrand: undefined;
readonly onDidChangeWorkspaceName = Event.None;
readonly onDidChangeWorkspaceFolders = Event.None;
readonly onDidChangeWorkbenchState = Event.None;
private readonly workspace: IWorkspace;
constructor() {
this.workspace = { id: '4064f6ec-cb38-4ad0-af64-ee6467e63c82', folders: [new WorkspaceFolder({ uri: workspaceResource, name: '', index: 0 })] };
}
async getCompleteWorkspace(): Promise<IWorkspace> { return this.getWorkspace(); }
getWorkspace(): IWorkspace { return this.workspace; }
getWorkbenchState(): WorkbenchState {
if (this.workspace) {
if (this.workspace.configuration) {
return WorkbenchState.WORKSPACE;
}
return WorkbenchState.FOLDER;
}
return WorkbenchState.EMPTY;
}
getWorkspaceFolder(resource: URI): IWorkspaceFolder | null { return resource && resource.scheme === workspaceResource.scheme ? this.workspace.folders[0] : null; }
isInsideWorkspace(resource: URI): boolean { return resource && resource.scheme === workspaceResource.scheme; }
isCurrentWorkspace(workspaceIdOrFolder: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | URI): boolean { return true; }
}
//#endregion
//#region Configuration
export class SimpleConfigurationService extends BaseSimpleConfigurationService implements IWorkbenchConfigurationService {
async whenRemoteConfigurationLoaded() { }
}
export const simpleWorkspace: ISingleFolderWorkspaceIdentifier = { id: '4064f6ec-cb38-4ad0-af64-ee6467e63c82', uri: URI.file(isWindows ? '\\simpleWorkspace' : '/simpleWorkspace') };
//#endregion
@ -189,25 +146,27 @@ class SimpleFileSystemProvider extends InMemoryFileSystemProvider { }
export const simpleFileSystemProvider = new SimpleFileSystemProvider();
function createFile(parent: string, name: string, content: string = ''): void {
simpleFileSystemProvider.writeFile(joinPath(workspaceResource, parent, name), VSBuffer.fromString(content).buffer, { create: true, overwrite: true });
simpleFileSystemProvider.mkdir(userDataDir);
function createWorkspaceFile(parent: string, name: string, content: string = ''): void {
simpleFileSystemProvider.writeFile(joinPath(simpleWorkspace.uri, parent, name), VSBuffer.fromString(content).buffer, { create: true, overwrite: true, unlock: false });
}
function createFolder(name: string): void {
simpleFileSystemProvider.mkdir(joinPath(workspaceResource, name));
function createWorkspaceFolder(name: string): void {
simpleFileSystemProvider.mkdir(joinPath(simpleWorkspace.uri, name));
}
createFolder('');
createFolder('src');
createFolder('test');
createWorkspaceFolder('');
createWorkspaceFolder('src');
createWorkspaceFolder('test');
createFile('', '.gitignore', `out
createWorkspaceFile('', '.gitignore', `out
node_modules
.vscode-test/
*.vsix
`);
createFile('', '.vscodeignore', `.vscode/**
createWorkspaceFile('', '.vscodeignore', `.vscode/**
.vscode-test/**
out/test/**
src/**
@ -218,14 +177,14 @@ vsc-extension-quickstart.md
**/*.map
**/*.ts`);
createFile('', 'CHANGELOG.md', `# Change Log
createWorkspaceFile('', 'CHANGELOG.md', `# Change Log
All notable changes to the "test-ts" extension will be documented in this file.
Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file.
## [Unreleased]
- Initial release`);
createFile('', 'package.json', `{
createWorkspaceFile('', 'package.json', `{
"name": "test-ts",
"displayName": "test-ts",
"description": "",
@ -265,7 +224,7 @@ createFile('', 'package.json', `{
}
`);
createFile('', 'tsconfig.json', `{
createWorkspaceFile('', 'tsconfig.json', `{
"compilerOptions": {
"module": "commonjs",
"target": "es6",
@ -288,7 +247,7 @@ createFile('', 'tsconfig.json', `{
}
`);
createFile('', 'tslint.json', `{
createWorkspaceFile('', 'tslint.json', `{
"rules": {
"no-string-throw": true,
"no-unused-expression": true,
@ -305,7 +264,7 @@ createFile('', 'tslint.json', `{
}
`);
createFile('src', 'extension.ts', `// The module 'vscode' contains the VS Code extensibility API
createWorkspaceFile('src', 'extension.ts', `// The module 'vscode' contains the VS Code extensibility API
// Import the module and reference it with the alias vscode in your code below
import * as vscode from 'vscode';
@ -334,7 +293,7 @@ export function activate(context: vscode.ExtensionContext) {
export function deactivate() {}
`);
createFile('test', 'extension.test.ts', `//
createWorkspaceFile('test', 'extension.test.ts', `//
// Note: This example test is leveraging the Mocha test framework.
// Please refer to their documentation on https://mochajs.org/ for help.
//
@ -357,7 +316,7 @@ suite("Extension Tests", function () {
});
});`);
createFile('test', 'index.ts', `//
createWorkspaceFile('test', 'index.ts', `//
// PLEASE DO NOT MODIFY / DELETE UNLESS YOU KNOW WHAT YOU ARE DOING
//
// This file is providing the test runner to use when running extension tests.
@ -409,17 +368,6 @@ registerSingleton(IWebviewService, SimpleWebviewService);
//#endregion
//#region Textfiles
class SimpleTextFileService extends AbstractTextFileService {
declare readonly _serviceBrand: undefined;
}
registerSingleton(ITextFileService, SimpleTextFileService);
//#endregion
//#region Tunnel
class SimpleTunnelService implements ITunnelService {
@ -496,3 +444,5 @@ registerSingleton(ITaskService, SimpleTaskService);
class SimpleTerminalInstanceService extends TerminalInstanceService { }
registerSingleton(ITerminalInstanceService, SimpleTerminalInstanceService);
//#endregion

View file

@ -0,0 +1,342 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { zoomLevelToZoomFactor } from 'vs/platform/windows/common/windows';
import { mark } from 'vs/base/common/performance';
import { Workbench } from 'vs/workbench/browser/workbench';
import { NativeWindow } from 'vs/workbench/electron-sandbox/window';
import { setZoomLevel, setZoomFactor, setFullscreen } from 'vs/base/browser/browser';
import { domContentLoaded } from 'vs/base/browser/dom';
import { onUnexpectedError } from 'vs/base/common/errors';
import { URI } from 'vs/base/common/uri';
import { WorkspaceService } from 'vs/workbench/services/configuration/browser/configurationService';
import { INativeWorkbenchConfiguration, INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-sandbox/environmentService';
import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection';
import { isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier, IWorkspaceInitializationPayload, reviveIdentifier } from 'vs/platform/workspaces/common/workspaces';
import { ILoggerService, ILogService } from 'vs/platform/log/common/log';
import { NativeStorageService } from 'vs/platform/storage/electron-sandbox/storageService';
import { Schemas } from 'vs/base/common/network';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { IWorkbenchConfigurationService } from 'vs/workbench/services/configuration/common/configuration';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { Disposable } from 'vs/base/common/lifecycle';
import { IMainProcessService } from 'vs/platform/ipc/electron-sandbox/services';
import { RemoteAuthorityResolverService } from 'vs/platform/remote/electron-sandbox/remoteAuthorityResolverService';
import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remoteAuthorityResolver';
import { RemoteAgentService } from 'vs/workbench/services/remote/electron-sandbox/remoteAgentServiceImpl';
import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService';
import { FileService } from 'vs/platform/files/common/fileService';
import { IFileService } from 'vs/platform/files/common/files';
import { RemoteFileSystemProvider } from 'vs/workbench/services/remote/common/remoteAgentFileSystemChannel';
import { ConfigurationCache } from 'vs/workbench/services/configuration/electron-sandbox/configurationCache';
import { ISignService } from 'vs/platform/sign/common/sign';
import { basename } from 'vs/base/common/path';
import { IProductService } from 'vs/platform/product/common/productService';
import { INativeHostService } from 'vs/platform/native/electron-sandbox/native';
import { NativeHostService } from 'vs/platform/native/electron-sandbox/nativeHostService';
import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity';
import { UriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentityService';
import { KeyboardLayoutService } from 'vs/workbench/services/keybinding/electron-sandbox/nativeKeyboardLayout';
import { IKeyboardLayoutService } from 'vs/platform/keyboardLayout/common/keyboardLayout';
import { ElectronIPCMainProcessService } from 'vs/platform/ipc/electron-sandbox/mainProcessService';
import { LoggerChannelClient } from 'vs/platform/log/common/logIpc';
import { ProxyChannel } from 'vs/base/parts/ipc/common/ipc';
import product from 'vs/platform/product/common/product';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
export const productService = { _serviceBrand: undefined, ...product };
export abstract class SharedDesktopMain extends Disposable {
protected readonly productService: IProductService = productService;
constructor(
protected readonly configuration: INativeWorkbenchConfiguration,
protected readonly environmentService: INativeWorkbenchEnvironmentService
) {
super();
this.init();
}
private init(): void {
// Massage configuration file URIs
this.reviveUris();
// Browser config
const zoomLevel = this.configuration.zoomLevel || 0;
setZoomFactor(zoomLevelToZoomFactor(zoomLevel));
setZoomLevel(zoomLevel, true /* isTrusted */);
setFullscreen(!!this.configuration.fullscreen);
}
private reviveUris() {
// Workspace
const workspace = reviveIdentifier(this.configuration.workspace);
if (isWorkspaceIdentifier(workspace) || isSingleFolderWorkspaceIdentifier(workspace)) {
this.configuration.workspace = workspace;
}
// Files
const filesToWait = this.configuration.filesToWait;
const filesToWaitPaths = filesToWait?.paths;
[filesToWaitPaths, this.configuration.filesToOpenOrCreate, this.configuration.filesToDiff].forEach(paths => {
if (Array.isArray(paths)) {
paths.forEach(path => {
if (path.fileUri) {
path.fileUri = URI.revive(path.fileUri);
}
});
}
});
if (filesToWait) {
filesToWait.waitMarkerFileUri = URI.revive(filesToWait.waitMarkerFileUri);
}
}
async open(): Promise<void> {
const services = await this.initServices();
await domContentLoaded();
mark('code/willStartWorkbench');
// Create Workbench
const workbench = new Workbench(document.body, services.serviceCollection, services.logService);
// Listeners
this.registerListeners(workbench, services.storageService);
// Startup
const instantiationService = workbench.startup();
// Window
this._register(instantiationService.createInstance(NativeWindow));
// Logging
services.logService.trace('workbench configuration', JSON.stringify(this.configuration));
// Allow subclass to participate
this.joinOpen(instantiationService);
}
private registerListeners(workbench: Workbench, storageService: NativeStorageService): void {
// Workbench Lifecycle
this._register(workbench.onWillShutdown(event => event.join(storageService.close(), 'join.closeStorage')));
this._register(workbench.onShutdown(() => this.dispose()));
}
protected abstract createLogService(loggerService: ILoggerService, mainProcessService: IMainProcessService): ILogService;
protected abstract registerFileSystemProviders(fileService: IFileService, logService: ILogService, nativeHostService: INativeHostService): void;
protected joinOpen(instantiationService: IInstantiationService): void { }
private async initServices(): Promise<{ serviceCollection: ServiceCollection, logService: ILogService, storageService: NativeStorageService }> {
const serviceCollection = new ServiceCollection();
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
//
// NOTE: Please do NOT register services here. Use `registerSingleton()`
// from `workbench.common.main.ts` if the service is shared between
// desktop and web or `workbench.sandbox.main.ts` if the service
// is desktop only.
//
// DO NOT add services to `workbench.desktop.main.ts`, always add
// to `workbench.sandbox.main.ts` to support our Electron sandbox
//
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
// Main Process
const mainProcessService = this._register(new ElectronIPCMainProcessService(this.configuration.windowId));
serviceCollection.set(IMainProcessService, mainProcessService);
// Environment
serviceCollection.set(INativeWorkbenchEnvironmentService, this.environmentService);
// Product
serviceCollection.set(IProductService, this.productService);
// Logger
const loggerService = new LoggerChannelClient(mainProcessService.getChannel('logger'));
serviceCollection.set(ILoggerService, loggerService);
// Log
const logService = this.createLogService(loggerService, mainProcessService);
serviceCollection.set(ILogService, logService);
// Remote
const remoteAuthorityResolverService = new RemoteAuthorityResolverService();
serviceCollection.set(IRemoteAuthorityResolverService, remoteAuthorityResolverService);
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
//
// NOTE: Please do NOT register services here. Use `registerSingleton()`
// from `workbench.common.main.ts` if the service is shared between
// desktop and web or `workbench.sandbox.main.ts` if the service
// is desktop only.
//
// DO NOT add services to `workbench.desktop.main.ts`, always add
// to `workbench.sandbox.main.ts` to support our Electron sandbox
//
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
// Sign
const signService = ProxyChannel.toService<ISignService>(mainProcessService.getChannel('sign'));
serviceCollection.set(ISignService, signService);
// Remote Agent
const remoteAgentService = this._register(new RemoteAgentService(this.environmentService, this.productService, remoteAuthorityResolverService, signService, logService));
serviceCollection.set(IRemoteAgentService, remoteAgentService);
// Native Host
const nativeHostService = new NativeHostService(this.configuration.windowId, mainProcessService) as INativeHostService;
serviceCollection.set(INativeHostService, nativeHostService);
// Files
const fileService = this._register(new FileService(logService));
serviceCollection.set(IFileService, fileService);
this.registerFileSystemProviders(fileService, logService, nativeHostService);
// Uri Identity
const uriIdentityService = new UriIdentityService(fileService);
serviceCollection.set(IUriIdentityService, uriIdentityService);
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
//
// NOTE: Please do NOT register services here. Use `registerSingleton()`
// from `workbench.common.main.ts` if the service is shared between
// desktop and web or `workbench.sandbox.main.ts` if the service
// is desktop only.
//
// DO NOT add services to `workbench.desktop.main.ts`, always add
// to `workbench.sandbox.main.ts` to support our Electron sandbox
//
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
const connection = remoteAgentService.getConnection();
if (connection) {
const remoteFileSystemProvider = this._register(new RemoteFileSystemProvider(remoteAgentService));
fileService.registerProvider(Schemas.vscodeRemote, remoteFileSystemProvider);
}
const payload = this.resolveWorkspaceInitializationPayload();
const services = await Promise.all([
this.createWorkspaceService(payload, fileService, remoteAgentService, uriIdentityService, logService).then(service => {
// Workspace
serviceCollection.set(IWorkspaceContextService, service);
// Configuration
serviceCollection.set(IWorkbenchConfigurationService, service);
return service;
}),
this.createStorageService(payload, mainProcessService).then(service => {
// Storage
serviceCollection.set(IStorageService, service);
return service;
}),
this.createKeyboardLayoutService(mainProcessService).then(service => {
// KeyboardLayout
serviceCollection.set(IKeyboardLayoutService, service);
return service;
})
]);
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
//
// NOTE: Please do NOT register services here. Use `registerSingleton()`
// from `workbench.common.main.ts` if the service is shared between
// desktop and web or `workbench.sandbox.main.ts` if the service
// is desktop only.
//
// DO NOT add services to `workbench.desktop.main.ts`, always add
// to `workbench.sandbox.main.ts` to support our Electron sandbox
//
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
return { serviceCollection, logService, storageService: services[1] };
}
private resolveWorkspaceInitializationPayload(): IWorkspaceInitializationPayload {
let workspaceInitializationPayload: IWorkspaceInitializationPayload | undefined = this.configuration.workspace;
// Fallback to empty workspace if we have no payload yet.
if (!workspaceInitializationPayload) {
let id: string;
if (this.configuration.backupPath) {
id = basename(this.configuration.backupPath); // we know the backupPath must be a unique path so we leverage its name as workspace ID
} else if (this.environmentService.isExtensionDevelopment) {
id = 'ext-dev'; // extension development window never stores backups and is a singleton
} else {
throw new Error('Unexpected window configuration without backupPath');
}
workspaceInitializationPayload = { id };
}
return workspaceInitializationPayload;
}
private async createWorkspaceService(payload: IWorkspaceInitializationPayload, fileService: FileService, remoteAgentService: IRemoteAgentService, uriIdentityService: IUriIdentityService, logService: ILogService): Promise<WorkspaceService> {
const workspaceService = new WorkspaceService({ remoteAuthority: this.environmentService.remoteAuthority, configurationCache: new ConfigurationCache(URI.file(this.environmentService.userDataPath), fileService) }, this.environmentService, fileService, remoteAgentService, uriIdentityService, logService);
try {
await workspaceService.initialize(payload);
return workspaceService;
} catch (error) {
onUnexpectedError(error);
return workspaceService;
}
}
private async createStorageService(payload: IWorkspaceInitializationPayload, mainProcessService: IMainProcessService): Promise<NativeStorageService> {
const storageService = new NativeStorageService(payload, mainProcessService, this.environmentService);
try {
await storageService.initialize();
return storageService;
} catch (error) {
onUnexpectedError(error);
return storageService;
}
}
private async createKeyboardLayoutService(mainProcessService: IMainProcessService): Promise<KeyboardLayoutService> {
const keyboardLayoutService = new KeyboardLayoutService(mainProcessService);
try {
await keyboardLayoutService.initialize();
return keyboardLayoutService;
} catch (error) {
onUnexpectedError(error);
return keyboardLayoutService;
}
}
}

View file

@ -314,6 +314,11 @@ export interface IEditorGroupsService {
*/
mergeGroup(group: IEditorGroup | GroupIdentifier, target: IEditorGroup | GroupIdentifier, options?: IMergeGroupOptions): IEditorGroup;
/**
* Merge all editor groups into the active one.
*/
mergeAllGroups(): IEditorGroup;
/**
* Copy a group to a new group in the editor area.
*

View file

@ -315,6 +315,34 @@ suite('EditorGroupsService', () => {
part.dispose();
});
test('merge all groups', async () => {
const [part] = createPart();
const rootGroup = part.groups[0];
const input1 = new TestFileEditorInput(URI.file('foo/bar1'), TEST_EDITOR_INPUT_ID);
const input2 = new TestFileEditorInput(URI.file('foo/bar2'), TEST_EDITOR_INPUT_ID);
const input3 = new TestFileEditorInput(URI.file('foo/bar3'), TEST_EDITOR_INPUT_ID);
await rootGroup.openEditor(input1, EditorOptions.create({ pinned: true }));
const rightGroup = part.addGroup(rootGroup, GroupDirection.RIGHT);
await rightGroup.openEditor(input2, EditorOptions.create({ pinned: true }));
const downGroup = part.copyGroup(rootGroup, rightGroup, GroupDirection.DOWN);
await downGroup.openEditor(input3, EditorOptions.create({ pinned: true }));
part.activateGroup(rootGroup);
assert.strictEqual(rootGroup.count, 1);
const result = part.mergeAllGroups();
assert.strictEqual(result.id, rootGroup.id);
assert.strictEqual(rootGroup.count, 3);
part.dispose();
});
test('whenRestored', async () => {
const [part] = createPart();

View file

@ -16,6 +16,7 @@ import { memoize } from 'vs/base/common/decorators';
import { onUnexpectedError } from 'vs/base/common/errors';
import { parseLineAndColumnAware } from 'vs/base/common/extpath';
import { LogLevelToString } from 'vs/platform/log/common/log';
import { ExtensionKind } from 'vs/platform/extensions/common/extensions';
class BrowserWorkbenchConfiguration implements IWindowConfiguration {
@ -86,6 +87,7 @@ interface IExtensionHostDebugEnvironment {
debugRenderer: boolean;
isExtensionDevelopment: boolean;
extensionDevelopmentLocationURI?: URI[];
extensionDevelopmentKind?: ExtensionKind[];
extensionTestsLocationURI?: URI;
extensionEnabledProposedApi?: string[];
}
@ -193,6 +195,14 @@ export class BrowserWorkbenchEnvironmentService implements IWorkbenchEnvironment
return this._extensionHostDebugEnvironment.extensionDevelopmentLocationURI;
}
get extensionDevelopmentLocationKind(): ExtensionKind[] | undefined {
if (!this._extensionHostDebugEnvironment) {
this._extensionHostDebugEnvironment = this.resolveExtensionHostDebugEnvironment();
}
return this._extensionHostDebugEnvironment.extensionDevelopmentKind;
}
get extensionTestsLocationURI(): URI | undefined {
if (!this._extensionHostDebugEnvironment) {
this._extensionHostDebugEnvironment = this.resolveExtensionHostDebugEnvironment();
@ -272,7 +282,8 @@ export class BrowserWorkbenchEnvironmentService implements IWorkbenchEnvironment
},
debugRenderer: false,
isExtensionDevelopment: false,
extensionDevelopmentLocationURI: undefined
extensionDevelopmentLocationURI: undefined,
extensionDevelopmentKind: undefined
};
// Fill in selected extra environmental properties
@ -283,6 +294,9 @@ export class BrowserWorkbenchEnvironmentService implements IWorkbenchEnvironment
extensionHostDebugEnvironment.extensionDevelopmentLocationURI = [URI.parse(value)];
extensionHostDebugEnvironment.isExtensionDevelopment = true;
break;
case 'extensionDevelopmentKind':
extensionHostDebugEnvironment.extensionDevelopmentKind = [<ExtensionKind>value];
break;
case 'extensionTestsPath':
extensionHostDebugEnvironment.extensionTestsLocationURI = URI.parse(value);
break;

View file

@ -50,7 +50,7 @@ export class ExtensionService extends AbstractExtensionService implements IExten
) {
super(
new ExtensionRunningLocationClassifier(
manifest => this._getExtensionKind(manifest),
(extension) => this._getExtensionKind(extension),
(extensionKinds, isInstalledLocally, isInstalledRemotely) => ExtensionService.pickRunningLocation(extensionKinds, isInstalledLocally, isInstalledRemotely)
),
instantiationService,

View file

@ -21,7 +21,7 @@ import { ExtensionMessageCollector, ExtensionPoint, ExtensionsRegistry, IExtensi
import { ExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/common/extensionDescriptionRegistry';
import { ResponsiveState } from 'vs/workbench/services/extensions/common/rpcProtocol';
import { ExtensionHostManager } from 'vs/workbench/services/extensions/common/extensionHostManager';
import { ExtensionIdentifier, IExtensionDescription, ExtensionType, ITranslatedScannedExtension, IExtension, ExtensionKind, IExtensionContributions, IExtensionManifest } from 'vs/platform/extensions/common/extensions';
import { ExtensionIdentifier, IExtensionDescription, ExtensionType, ITranslatedScannedExtension, IExtension, ExtensionKind, IExtensionContributions } from 'vs/platform/extensions/common/extensions';
import { IFileService } from 'vs/platform/files/common/files';
import { parseExtensionDevOptions } from 'vs/workbench/services/extensions/common/extensionDevOptions';
import { IProductService } from 'vs/platform/product/common/productService';
@ -173,8 +173,12 @@ export abstract class AbstractExtensionService extends Disposable implements IEx
}));
}
protected _getExtensionKind(manifest: IExtensionManifest): ExtensionKind[] {
return this._extensionKindController.getExtensionKind(manifest);
protected _getExtensionKind(extensionDescription: IExtensionDescription): ExtensionKind[] {
if (extensionDescription.isUnderDevelopment && this._environmentService.extensionDevelopmentKind) {
return this._environmentService.extensionDevelopmentKind;
}
return this._extensionKindController.getExtensionKind(extensionDescription);
}
protected _getExtensionHostManager(kind: ExtensionHostKind): ExtensionHostManager | null {
@ -211,16 +215,16 @@ export abstract class AbstractExtensionService extends Disposable implements IEx
for (let i = 0, len = _toAdd.length; i < len; i++) {
const extension = _toAdd[i];
if (!this._canAddExtension(extension)) {
continue;
}
const extensionDescription = await this._scanSingleExtension(extension);
if (!extensionDescription) {
// could not scan extension...
continue;
}
if (!this.canAddExtension(extensionDescription)) {
continue;
}
toAdd.push(extensionDescription);
}
@ -310,24 +314,20 @@ export abstract class AbstractExtensionService extends Disposable implements IEx
await Promise.all(promises);
}
public canAddExtension(extensionDescription: IExtensionDescription): boolean {
return this._canAddExtension(toExtension(extensionDescription));
}
private _canAddExtension(extension: IExtension): boolean {
const extensionDescription = this._registry.getExtensionDescription(extension.identifier.id);
if (extensionDescription) {
public canAddExtension(extension: IExtensionDescription): boolean {
const existing = this._registry.getExtensionDescription(extension.identifier);
if (existing) {
// this extension is already running (most likely at a different version)
return false;
}
// Check if extension is renamed
if (extension.identifier.uuid && this._registry.getAllExtensionDescriptions().some(e => e.uuid === extension.identifier.uuid)) {
if (extension.uuid && this._registry.getAllExtensionDescriptions().some(e => e.uuid === extension.uuid)) {
return false;
}
const extensionKind = this._getExtensionKind(extension.manifest);
const isRemote = extension.location.scheme === Schemas.vscodeRemote;
const extensionKind = this._getExtensionKind(extension);
const isRemote = extension.extensionLocation.scheme === Schemas.vscodeRemote;
const runningLocation = this._runningLocationClassifier.pickRunningLocation(extensionKind, !isRemote, isRemote);
if (runningLocation === ExtensionRunningLocation.None) {
return false;
@ -420,6 +420,43 @@ export abstract class AbstractExtensionService extends Disposable implements IEx
await this._scanAndHandleExtensions();
this._releaseBarrier();
perf.mark('code/didLoadExtensions');
await this._handleExtensionTests();
}
private async _handleExtensionTests(): Promise<void> {
if (!this._environmentService.extensionDevelopmentLocationURI || !this._environmentService.extensionTestsLocationURI) {
return;
}
// TODO: Use `this._registry` and `this._runningLocation` to better determine which extension host should launch the test runner
let extensionHostManager: ExtensionHostManager | null = null;
if (this._environmentService.extensionTestsLocationURI.scheme === Schemas.vscodeRemote) {
extensionHostManager = this._getExtensionHostManager(ExtensionHostKind.Remote);
}
if (!extensionHostManager) {
// When a debugger attaches to the extension host, it will surface all console.log messages from the extension host,
// but not necessarily from the window. So it would be best if any errors get printed to the console of the extension host.
// That is why here we use the local process extension host even for non-file URIs
extensionHostManager = this._getExtensionHostManager(ExtensionHostKind.LocalProcess);
}
if (!extensionHostManager) {
const msg = nls.localize('extensionTestError', "No extension host found that can launch the test runner at {0}.", this._environmentService.extensionTestsLocationURI.toString());
console.error(msg);
this._notificationService.error(msg);
return;
}
let exitCode: number;
try {
exitCode = await extensionHostManager.extensionTestsExecute();
} catch (err) {
console.error(err);
exitCode = 1 /* ERROR */;
}
await extensionHostManager.extensionTestsSendExit(exitCode);
this._onExtensionHostExit(exitCode);
}
private _releaseBarrier(): void {
@ -779,7 +816,7 @@ export abstract class AbstractExtensionService extends Disposable implements IEx
export class ExtensionRunningLocationClassifier {
constructor(
public readonly getExtensionKind: (manifest: IExtensionManifest) => ExtensionKind[],
public readonly getExtensionKind: (extensionDescription: IExtensionDescription) => ExtensionKind[],
public readonly pickRunningLocation: (extensionKinds: ExtensionKind[], isInstalledLocally: boolean, isInstalledRemotely: boolean) => ExtensionRunningLocation,
) {
}

View file

@ -23,6 +23,7 @@ import { VSBuffer } from 'vs/base/common/buffer';
import { IExtensionHost, ExtensionHostKind, ActivationKind } from 'vs/workbench/services/extensions/common/extensions';
import { ExtensionActivationReason } from 'vs/workbench/api/common/extHostExtensionActivator';
import { CATEGORIES } from 'vs/workbench/common/actions';
import { timeout } from 'vs/base/common/async';
// Enable to see detailed message communication between window and extension host
const LOG_EXTENSION_HOST_COMMUNICATION = false;
@ -292,6 +293,28 @@ export class ExtensionHostManager extends Disposable {
return proxy.$startExtensionHost(enabledExtensionIds);
}
public async extensionTestsExecute(): Promise<number> {
const proxy = await this._getProxy();
if (!proxy) {
throw new Error('Could not obtain Extension Host Proxy');
}
return proxy.$extensionTestsExecute();
}
public async extensionTestsSendExit(exitCode: number): Promise<void> {
const proxy = await this._getProxy();
if (!proxy) {
return;
}
// This method does not wait for the actual RPC to be confirmed
// It waits for the socket to drain (i.e. the message has been sent)
// It also times out after 5s in case drain takes too long
proxy.$extensionTestsExit(exitCode);
if (this._rpcProtocol) {
await Promise.race([this._rpcProtocol.drain(), timeout(5000)]);
}
}
public async deltaExtensions(toAdd: IExtensionDescription[], toRemove: ExtensionIdentifier[]): Promise<void> {
const proxy = await this._getProxy();
if (!proxy) {

View file

@ -253,7 +253,6 @@ export interface IExtensionService {
_onWillActivateExtension(extensionId: ExtensionIdentifier): void;
_onDidActivateExtension(extensionId: ExtensionIdentifier, codeLoadingTime: number, activateCallTime: number, activateResolvedTime: number, activationReason: ExtensionActivationReason): void;
_onExtensionRuntimeError(extensionId: ExtensionIdentifier, err: Error): void;
_onExtensionHostExit(code: number): void;
}
export interface ProfileSession {

View file

@ -70,7 +70,7 @@ export class ExtensionService extends AbstractExtensionService implements IExten
) {
super(
new ExtensionRunningLocationClassifier(
manifest => this._getExtensionKind(manifest),
(extension) => this._getExtensionKind(extension),
(extensionKinds, isInstalledLocally, isInstalledRemotely) => this._pickRunningLocation(extensionKinds, isInstalledLocally, isInstalledRemotely)
),
instantiationService,
@ -85,7 +85,7 @@ export class ExtensionService extends AbstractExtensionService implements IExten
configurationService,
);
this._enableLocalWebWorker = this._configurationService.getValue<boolean>(webWorkerExtHostConfig);
this._enableLocalWebWorker = this._isLocalWebWorkerEnabled();
this._remoteInitData = new Map<string, IRemoteExtensionHostInitData>();
this._extensionScanner = instantiationService.createInstance(CachedExtensionScanner);
@ -103,6 +103,16 @@ export class ExtensionService extends AbstractExtensionService implements IExten
});
}
private _isLocalWebWorkerEnabled() {
if (this._configurationService.getValue<boolean>(webWorkerExtHostConfig)) {
return true;
}
if (this._environmentService.isExtensionDevelopment && this._environmentService.extensionDevelopmentKind?.some(k => k === 'web')) {
return true;
}
return false;
}
protected _scanSingleExtension(extension: IExtension): Promise<IExtensionDescription | null> {
if (extension.location.scheme === Schemas.vscodeRemote) {
return this._remoteAgentService.scanSingleExtension(extension.location, extension.type === ExtensionType.System);

View file

@ -209,6 +209,10 @@ export class LocalProcessExtensionHost implements IExtensionHost {
opts.execArgv.unshift('--prof');
}
if (this._environmentService.args['max-memory']) {
opts.execArgv.unshift(`--max-old-space-size=${this._environmentService.args['max-memory']}`);
}
// On linux crash reporter needs to be started on child node processes explicitly
if (platform.isLinux) {
const crashReporterStartOptions: CrashReporterStartOptions = {

View file

@ -79,6 +79,7 @@ export class RemoteFileSystemProvider extends Disposable implements
| FileSystemProviderCapabilities.FileOpenReadWriteClose
| FileSystemProviderCapabilities.FileReadStream
| FileSystemProviderCapabilities.FileFolderCopy
| FileSystemProviderCapabilities.FileWriteUnlock
);
if (isCaseSensitive) {

View file

@ -154,10 +154,10 @@ interface PortAttributes extends Attributes {
export class PortsAttributes extends Disposable {
private static SETTING = 'remote.portsAttributes';
private static DEFAULTS = 'remote.portsAttributes.defaults';
private static RANGE = /^(\d+)\-(\d+)$/;
private static OTHERS = 'others';
private portsAttributes: PortAttributes[] = [];
private otherPortAttributes: Attributes | undefined;
private defaultPortAttributes: Attributes | undefined;
private _onDidChangeAttributes = new Emitter<void>();
public readonly onDidChangeAttributes = this._onDidChangeAttributes.event;
@ -241,12 +241,6 @@ export class PortsAttributes extends Disposable {
let key: number | { start: number, end: number } | RegExp | undefined = undefined;
if (Number(attributesKey)) {
key = Number(attributesKey);
} else if (attributesKey === PortsAttributes.OTHERS) {
this.otherPortAttributes = {
elevateIfNeeded: setting.elevateIfPrivileged,
onAutoForward: setting.onAutoForward,
label: setting.label
};
} else if (isString(attributesKey)) {
if (PortsAttributes.RANGE.test(attributesKey)) {
const match = (<string>attributesKey).match(PortsAttributes.RANGE);
@ -269,6 +263,15 @@ export class PortsAttributes extends Disposable {
});
}
const defaults = <any>this.configurationService.getValue(PortsAttributes.DEFAULTS);
if (defaults) {
this.defaultPortAttributes = {
elevateIfNeeded: defaults.elevateIfNeeded,
label: defaults.label,
onAutoForward: defaults.onAutoForward
};
}
return this.sortAttributes(attributes);
}
@ -289,7 +292,7 @@ export class PortsAttributes extends Disposable {
}
private getOtherAttributes() {
return this.otherPortAttributes;
return this.defaultPortAttributes;
}
static providedActionToAction(providedAction: ProvidedOnAutoForward | undefined) {
@ -324,6 +327,7 @@ export class TunnelModel extends Disposable {
public onEnvironmentTunnelsSet: Event<void> = this._onEnvironmentTunnelsSet.event;
private _environmentTunnelsSet: boolean = false;
private configPortsAttributes: PortsAttributes;
private restoreListener: IDisposable | undefined;
private portAttributesProviders: PortAttributesProvider[] = [];
@ -426,18 +430,19 @@ export class TunnelModel extends Disposable {
await this.forward({ host: tunnel.remoteHost, port: tunnel.remotePort }, tunnel.localPort, tunnel.name, undefined, undefined, tunnel.privacy === TunnelPrivacy.Public);
}
}
} else {
// It's possible that at restore time the value hasn't synced.
const key = await this.getStorageKey();
const listener = this.storageService.onDidChangeValue(async (e) => {
if (e.key === key) {
listener.dispose();
this.tunnelRestoreValue = Promise.resolve(this.storageService.get(await this.getStorageKey(), StorageScope.GLOBAL));
await this.restoreForwarded();
}
});
}
}
if (!this.restoreListener) {
// It's possible that at restore time the value hasn't synced.
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.GLOBAL));
await this.restoreForwarded();
}
}));
}
}
private async storeForwarded() {

View file

@ -798,10 +798,10 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
return this.saveSequentializer.setPending(versionId, (async () => {
try {
const stat = await this.textFileService.write(lastResolvedFileStat.resource, resolvedTextFileEditorModel.createSnapshot(), {
overwriteReadonly: options.overwriteReadonly,
mtime: lastResolvedFileStat.mtime,
encoding: this.getEncoding(),
etag: (options.ignoreModifiedSince || !this.filesConfigurationService.preventSaveConflicts(lastResolvedFileStat.resource, resolvedTextFileEditorModel.getMode())) ? ETAG_DISABLED : lastResolvedFileStat.etag,
unlock: options.writeUnlock,
writeElevated: options.writeElevated
});

View file

@ -130,11 +130,6 @@ export interface IWriteTextFileOptions extends IWriteFileOptions {
*/
encoding?: string;
/**
* Whether to overwrite a file even if it is readonly.
*/
overwriteReadonly?: boolean;
/**
* Whether to write to the file as elevated (admin) user. When setting this option a prompt will
* ask the user to authenticate as super user.
@ -367,9 +362,9 @@ export interface ITextFileEditorModelManager {
export interface ITextFileSaveOptions extends ISaveOptions {
/**
* Makes the file writable if it is readonly.
* Save the file with an attempt to unlock it.
*/
overwriteReadonly?: boolean;
writeUnlock?: boolean;
/**
* Save the file with elevated privileges.

View file

@ -3,14 +3,12 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { promises } from 'fs';
import { localize } from 'vs/nls';
import { process } from 'vs/base/parts/sandbox/electron-sandbox/globals';
import { AbstractTextFileService } from 'vs/workbench/services/textfile/browser/textFileService';
import { ITextFileService, ITextFileStreamContent, ITextFileContent, IReadTextFileOptions, IWriteTextFileOptions, TextFileEditorModelState, ITextFileEditorModel } from 'vs/workbench/services/textfile/common/textfiles';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { URI } from 'vs/base/common/uri';
import { IFileStatWithMetadata, FileOperationError, FileOperationResult, IFileService, ByteSize, getPlatformLimits, Arch } from 'vs/platform/files/common/files';
import { IFileStatWithMetadata, IFileService, ByteSize, getPlatformLimits, Arch } from 'vs/platform/files/common/files';
import { Schemas } from 'vs/base/common/network';
import { join } from 'vs/base/common/path';
import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfigurationService';
@ -123,46 +121,13 @@ export class NativeTextFileService extends AbstractTextFileService {
async write(resource: URI, value: string | ITextSnapshot, options?: IWriteTextFileOptions): Promise<IFileStatWithMetadata> {
// check for overwriteReadonly property (only supported for local file://)
try {
if (options?.overwriteReadonly && resource.scheme === Schemas.file) {
const fileStat = await promises.stat(resource.fsPath);
// try to change mode to writeable
await promises.chmod(resource.fsPath, fileStat.mode | 0o200 /* File mode indicating writable by owner (fs.constants.S_IWUSR) */);
}
} catch (error) {
// ignore and simply retry the operation
}
// check for writeElevated property (only supported for local file://)
// check for `writeElevated property` to write elevated
// (file:// only: https://github.com/microsoft/vscode/issues/48659)
if (options?.writeElevated && resource.scheme === Schemas.file) {
return this.writeElevated(resource, value, options);
}
try {
return await super.write(resource, value, options);
} catch (error) {
// In case of permission denied, we need to check for readonly
if ((<FileOperationError>error).fileOperationResult === FileOperationResult.FILE_PERMISSION_DENIED) {
let isReadonly = false;
try {
const fileStat = await promises.stat(resource.fsPath);
if (!(fileStat.mode & 0o200 /* File mode indicating writable by owner (fs.constants.S_IWUSR) */)) {
isReadonly = true;
}
} catch (error) {
// ignore - rethrow original error
}
if (isReadonly) {
throw new FileOperationError(localize('fileReadOnlyError', "File is Read Only"), FileOperationResult.FILE_READ_ONLY, options);
}
}
throw error;
}
return super.write(resource, value, options);
}
private async writeElevated(resource: URI, value: string | ITextSnapshot, options?: IWriteTextFileOptions): Promise<IFileStatWithMetadata> {

View file

@ -57,7 +57,7 @@ if (isWeb) {
await fileProvider.writeFile(
URI.file(join(testDir, fileName)),
files[fileName],
{ create: true, overwrite: false }
{ create: true, overwrite: false, unlock: false }
);
}

View file

@ -641,6 +641,7 @@ export class TestEditorGroupsService implements IEditorGroupsService {
removeGroup(_group: number | IEditorGroup): void { }
moveGroup(_group: number | IEditorGroup, _location: number | IEditorGroup, _direction: GroupDirection): IEditorGroup { throw new Error('not implemented'); }
mergeGroup(_group: number | IEditorGroup, _target: number | IEditorGroup, _options?: IMergeGroupOptions): IEditorGroup { throw new Error('not implemented'); }
mergeAllGroups(): IEditorGroup { throw new Error('not implemented'); }
copyGroup(_group: number | IEditorGroup, _location: number | IEditorGroup, _direction: GroupDirection): IEditorGroup { throw new Error('not implemented'); }
centerLayout(active: boolean): void { }
isLayoutCentered(): boolean { return false; }

View file

@ -7,7 +7,7 @@ import { workbenchInstantiationService as browserWorkbenchInstantiationService,
import { Event } from 'vs/base/common/event';
import { ISharedProcessService } from 'vs/platform/ipc/electron-sandbox/services';
import { NativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-browser/environmentService';
import { NativeTextFileService, } from 'vs/workbench/services/textfile/electron-browser/nativeTextFileService';
import { NativeTextFileService, } from 'vs/workbench/services/textfile/electron-sandbox/nativeTextFileService';
import { INativeHostService } from 'vs/platform/native/electron-sandbox/native';
import { FileOperationError, IFileService } from 'vs/platform/files/common/files';
import { IUntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService';
@ -201,7 +201,7 @@ export class TestNativeHostService implements INativeHostService {
async showItemInFolder(path: string): Promise<void> { }
async setRepresentedFilename(path: string): Promise<void> { }
async isAdmin(): Promise<boolean> { return false; }
async writeElevated(source: URI, target: URI, options?: { overwriteReadonly?: boolean | undefined; }): Promise<void> { }
async writeElevated(source: URI, target: URI): Promise<void> { }
async getOSProperties(): Promise<IOSProperties> { return Object.create(null); }
async getOSStatistics(): Promise<IOSStatistics> { return Object.create(null); }
async getOSVirtualMachineHint(): Promise<number> { return 0; }

View file

@ -58,7 +58,6 @@ import 'vs/workbench/electron-browser/desktop.main';
import 'vs/workbench/services/search/electron-browser/searchService';
import 'vs/workbench/services/textfile/electron-browser/nativeTextFileService';
import 'vs/workbench/services/extensions/electron-browser/extensionService';
import 'vs/workbench/services/remote/electron-browser/tunnelServiceImpl';

View file

@ -26,6 +26,7 @@ import 'vs/workbench/electron-sandbox/parts/dialogs/dialog.contribution';
//#region --- workbench services
import 'vs/workbench/services/textfile/electron-sandbox/nativeTextFileService';
import 'vs/workbench/services/dialogs/electron-sandbox/fileDialogService';
import 'vs/workbench/services/workspaces/electron-sandbox/workspacesService';
import 'vs/workbench/services/textMate/electron-sandbox/textMateService';