diff --git a/build/package.json b/build/package.json index 50b8a85f39..8fe67192a0 100644 --- a/build/package.json +++ b/build/package.json @@ -43,7 +43,7 @@ "request": "^2.85.0", "tslint": "^5.9.1", "service-downloader": "github:anthonydresser/service-downloader#0.1.5", - "typescript": "3.4.1", + "typescript": "3.4.5", "vsce": "1.48.0", "xml2js": "^0.4.17" }, diff --git a/build/yarn.lock b/build/yarn.lock index c1f72ff764..7a77cf1451 100644 --- a/build/yarn.lock +++ b/build/yarn.lock @@ -3201,10 +3201,10 @@ typed-rest-client@^0.9.0: tunnel "0.0.4" underscore "1.8.3" -typescript@3.4.1: - version "3.4.1" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.4.1.tgz#b6691be11a881ffa9a05765a205cb7383f3b63c6" - integrity sha512-3NSMb2VzDQm8oBTLH6Nj55VVtUEpe/rgkIzMir0qVoLyjDZlnMBva0U6vDiV3IH+sl/Yu6oP5QwsAQtHPmDd2Q== +typescript@3.4.5: + version "3.4.5" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.4.5.tgz#2d2618d10bb566572b8d7aad5180d84257d70a99" + integrity sha512-YycBxUb49UUhdNMU5aJ7z5Ej2XGmaIBL0x34vZ82fn3hGvD+bgrMrVDpatgz2f7YxUMJxMkbWxJZeAvDxVe7Vw== uc.micro@^1.0.1, uc.micro@^1.0.5: version "1.0.5" diff --git a/extensions/markdown-language-features/package.json b/extensions/markdown-language-features/package.json index b204fb78f5..e18099fe67 100644 --- a/extensions/markdown-language-features/package.json +++ b/extensions/markdown-language-features/package.json @@ -10,7 +10,6 @@ "vscode": "^1.20.0" }, "main": "./out/extension", - "extensionKind": "ui", "categories": [ "Programming Languages" ], diff --git a/extensions/package.json b/extensions/package.json index 75b3235c7c..8d41ef689c 100644 --- a/extensions/package.json +++ b/extensions/package.json @@ -3,7 +3,7 @@ "version": "0.0.1", "description": "Dependencies shared by all extensions", "dependencies": { - "typescript": "3.4.4" + "typescript": "3.4.5" }, "scripts": { "postinstall": "node ./postinstall" diff --git a/extensions/yarn.lock b/extensions/yarn.lock index 4357f5258d..149346bbb9 100644 --- a/extensions/yarn.lock +++ b/extensions/yarn.lock @@ -2,7 +2,7 @@ # yarn lockfile v1 -typescript@3.4.4: - version "3.4.4" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.4.4.tgz#aac4a08abecab8091a75f10842ffa0631818f785" - integrity sha512-xt5RsIRCEaf6+j9AyOBgvVuAec0i92rgCaS3S+UVf5Z/vF2Hvtsw08wtUTJqp4djwznoAgjSxeCcU4r+CcDBJA== +typescript@3.4.5: + version "3.4.5" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.4.5.tgz#2d2618d10bb566572b8d7aad5180d84257d70a99" + integrity sha512-YycBxUb49UUhdNMU5aJ7z5Ej2XGmaIBL0x34vZ82fn3hGvD+bgrMrVDpatgz2f7YxUMJxMkbWxJZeAvDxVe7Vw== diff --git a/package.json b/package.json index 228214e4b5..2a2be6a95b 100644 --- a/package.json +++ b/package.json @@ -158,7 +158,7 @@ "tslint": "^5.11.0", "tslint-microsoft-contrib": "^6.0.0", "typemoq": "^0.3.2", - "typescript": "3.4.1", + "typescript": "3.4.5", "typescript-formatter": "7.1.0", "typescript-tslint-plugin": "^0.0.7", "uglify-es": "^3.0.18", diff --git a/resources/win32/bin/code.sh b/resources/win32/bin/code.sh index 75ee9c493c..a4a621417e 100644 --- a/resources/win32/bin/code.sh +++ b/resources/win32/bin/code.sh @@ -6,29 +6,49 @@ COMMIT="@@COMMIT@@" APP_NAME="@@APPNAME@@" QUALITY="@@QUALITY@@" NAME="@@NAME@@" - +VSCODE_PATH="$(dirname "$(dirname "$(realpath "$0")")")" +ELECTRON="$VSCODE_PATH/$NAME.exe" if grep -qi Microsoft /proc/version; then # in a wsl shell - WIN_CODE_CMD=$(wslpath -w "$(dirname "$(realpath "$0")")/$APP_NAME.cmd") - if ! [ -z "$WIN_CODE_CMD" ]; then + fallback() { + # If running under older WSL, don't pass cli.js to Electron as + # environment vars cannot be transferred from WSL to Windows + # See: https://github.com/Microsoft/BashOnWindows/issues/1363 + # https://github.com/Microsoft/BashOnWindows/issues/1494 + "$ELECTRON" "$@" + exit $? + } + WSL_BUILD=$(uname -r | sed -E 's/^.+-([0-9]+)-Microsoft/\1/') + # wslpath is not available prior to WSL build 17046 + # See: https://docs.microsoft.com/en-us/windows/wsl/release-notes#build-17046 + if [ -x /bin/wslpath ]; then + WIN_CODE_CMD=$(wslpath -w "$(dirname "$(realpath "$0")")/$APP_NAME.cmd") + # make sure the cwd is in the windows fs, otherwise there will be a warning from cmd + pushd "$(dirname "$0")" > /dev/null WSL_EXT_ID="ms-vscode.remote-wsl" WSL_EXT_WLOC=$(cmd.exe /c "$WIN_CODE_CMD" --locate-extension $WSL_EXT_ID) + popd > /dev/null if ! [ -z "$WSL_EXT_WLOC" ]; then # replace \r\n with \n in WSL_EXT_WLOC, get linux path for WSL_CODE=$(wslpath -u "${WSL_EXT_WLOC%%[[:cntrl:]]}")/scripts/wslCode.sh "$WSL_CODE" $COMMIT $QUALITY "$WIN_CODE_CMD" "$APP_NAME" "$@" exit $? + elif [ $WSL_BUILD -ge 17063 ] 2> /dev/null; then + # Since WSL build 17063, we just need to set WSLENV so that + # ELECTRON_RUN_AS_NODE is visible to the win32 process + # See: https://docs.microsoft.com/en-us/windows/wsl/release-notes#build-17063 + export WSLENV=ELECTRON_RUN_AS_NODE/w:$WSLENV + CLI=$(wslpath -m "$VSCODE_PATH/resources/app/out/cli.js") + else # $WSL_BUILD ∈ [17046, 17063) OR $WSL_BUILD is indeterminate + fallback "$@" fi + else + fallback "$@" fi -fi - -VSCODE_PATH="$(dirname "$(dirname "$(realpath "$0")")")" - -if [ -x "$(command -v cygpath)" ]; then +elif [ -x "$(command -v cygpath)" ]; then CLI=$(cygpath -m "$VSCODE_PATH/resources/app/out/cli.js") else CLI="$VSCODE_PATH/resources/app/out/cli.js" fi -ELECTRON="$VSCODE_PATH/$NAME.exe" ELECTRON_RUN_AS_NODE=1 "$ELECTRON" "$CLI" "$@" exit $? diff --git a/src/vs/base/browser/ui/dialog/dialog.ts b/src/vs/base/browser/ui/dialog/dialog.ts index b964e33fb8..92793eb2a5 100644 --- a/src/vs/base/browser/ui/dialog/dialog.ts +++ b/src/vs/base/browser/ui/dialog/dialog.ts @@ -40,7 +40,6 @@ export class Dialog extends Disposable { private buttonGroup: ButtonGroup | undefined; private styles: IDialogStyles | undefined; private focusToReturn: HTMLElement | undefined; - private iconRotatingInternal: any | undefined; constructor(private container: HTMLElement, private message: string, private buttons: string[], private options: IDialogOptions) { super(); @@ -163,15 +162,6 @@ export class Dialog extends Disposable { break; case 'pending': addClass(this.iconElement, 'icon-pending'); - let deg = 0; - this.iconRotatingInternal = setInterval(() => { - if (this.iconElement) { - this.iconElement.style.transform = `rotate(${deg}deg)`; - deg += 45; // 360 / 8 - } else { - this.iconRotatingInternal = undefined; - } - }, 125 /** 1000 / 8 */); break; case 'none': case 'info': @@ -233,10 +223,6 @@ export class Dialog extends Disposable { this.modal = undefined; } - if (this.iconRotatingInternal) { - this.iconRotatingInternal = undefined; - } - if (this.focusToReturn && isAncestor(this.focusToReturn, document.body)) { this.focusToReturn.focus(); this.focusToReturn = undefined; diff --git a/src/vs/base/browser/ui/dialog/pending-dark.svg b/src/vs/base/browser/ui/dialog/pending-dark.svg index 97810808c3..5f38838116 100644 --- a/src/vs/base/browser/ui/dialog/pending-dark.svg +++ b/src/vs/base/browser/ui/dialog/pending-dark.svg @@ -1,13 +1,31 @@ + - - - - - - - + + + + + + + diff --git a/src/vs/base/browser/ui/dialog/pending-hc.svg b/src/vs/base/browser/ui/dialog/pending-hc.svg index 73c63ba3ce..c6d0ec7e29 100644 --- a/src/vs/base/browser/ui/dialog/pending-hc.svg +++ b/src/vs/base/browser/ui/dialog/pending-hc.svg @@ -1,13 +1,31 @@ + - - - - - - - + + + + + + + diff --git a/src/vs/base/browser/ui/dialog/pending.svg b/src/vs/base/browser/ui/dialog/pending.svg index 113a96cfcf..47ce444bb2 100644 --- a/src/vs/base/browser/ui/dialog/pending.svg +++ b/src/vs/base/browser/ui/dialog/pending.svg @@ -1,13 +1,31 @@ + - - - - - - - + + + + + + + diff --git a/src/vs/base/common/marked/cgmanifest.json b/src/vs/base/common/marked/cgmanifest.json index b0c1ce8f3d..f3477931d1 100644 --- a/src/vs/base/common/marked/cgmanifest.json +++ b/src/vs/base/common/marked/cgmanifest.json @@ -6,11 +6,11 @@ "git": { "name": "marked", "repositoryUrl": "https://github.com/markedjs/marked", - "commitHash": "78c977bc3a47f9e2fb146477d1ca3dad0cb134e6" + "commitHash": "529a8d4e185a8aa561e4d8d2891f8556b5717cd4" } }, "license": "MIT", - "version": "0.5.0" + "version": "0.6.2" } ], "version": 1 diff --git a/src/vs/base/common/marked/marked.js b/src/vs/base/common/marked/marked.js index 1e63f272db..968f9029e9 100644 --- a/src/vs/base/common/marked/marked.js +++ b/src/vs/base/common/marked/marked.js @@ -23,7 +23,7 @@ var block = { heading: /^ *(#{1,6}) *([^\n]+?) *(?:#+ *)?(?:\n+|$)/, nptable: noop, blockquote: /^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/, - list: /^( *)(bull) [\s\S]+?(?:hr|def|\n{2,}(?! )(?!\1bull )\n*|\s*$)/, + list: /^( {0,3})(bull) [\s\S]+?(?:hr|def|\n{2,}(?! )(?!\1bull )\n*|\s*$)/, html: '^ {0,3}(?:' // optional indentation + '<(script|pre|style)[\\s>][\\s\\S]*?(?:[^\\n]*\\n+|$)' // (1) + '|comment[^\\n]*(\\n+|$)' // (2) @@ -31,8 +31,8 @@ var block = { + '|\\n*' // (4) + '|\\n*' // (5) + '|)[\\s\\S]*?(?:\\n{2,}|$)' // (6) - + '|<(?!script|pre|style)([a-z][\\w-]*)(?:attribute)*? */?>(?=\\h*\\n)[\\s\\S]*?(?:\\n{2,}|$)' // (7) open tag - + '|(?=\\h*\\n)[\\s\\S]*?(?:\\n{2,}|$)' // (7) closing tag + + '|<(?!script|pre|style)([a-z][\\w-]*)(?:attribute)*? */?>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:\\n{2,}|$)' // (7) open tag + + '|(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:\\n{2,}|$)' // (7) closing tag + ')', def: /^ {0,3}\[(label)\]: *\n? *]+)>?(?:(?: +\n? *| *\n *)(title))? *(?:\n+|$)/, table: noop, @@ -48,8 +48,8 @@ block.def = edit(block.def) .replace('title', block._title) .getRegex(); -block.bullet = /(?:[*+-]|\d+\.)/; -block.item = /^( *)(bull) [^\n]*(?:\n(?!\1bull )[^\n]*)*/; +block.bullet = /(?:[*+-]|\d{1,9}\.)/; +block.item = /^( *)(bull) ?[^\n]*(?:\n(?!\1bull ?)[^\n]*)*/; block.item = edit(block.item, 'gm') .replace(/bull/g, block.bullet) .getRegex(); @@ -95,7 +95,7 @@ block.normal = merge({}, block); */ block.gfm = merge({}, block.normal, { - fences: /^ *(`{3,}|~{3,})[ \.]*(\S+)? *\n([\s\S]*?)\n? *\1 *(?:\n+|$)/, + fences: /^ {0,3}(`{3,}|~{3,})([^`\n]*)\n(?:|([\s\S]*?)\n)(?: {0,3}\1[~`]* *(?:\n+|$)|$)/, paragraph: /^/, heading: /^ *(#{1,6}) +([^\n]+?) *#* *(?:\n+|$)/ }); @@ -235,7 +235,7 @@ Lexer.prototype.token = function(src, top) { src = src.substring(cap[0].length); this.tokens.push({ type: 'code', - lang: cap[2], + lang: cap[2] ? cap[2].trim() : cap[2], text: cap[3] || '' }); continue; @@ -253,7 +253,7 @@ Lexer.prototype.token = function(src, top) { } // table no leading pipe (gfm) - if (top && (cap = this.rules.nptable.exec(src))) { + if (cap = this.rules.nptable.exec(src)) { item = { type: 'table', header: splitCells(cap[1].replace(/^ *| *\| *$/g, '')), @@ -346,7 +346,7 @@ Lexer.prototype.token = function(src, top) { // Remove the list item's bullet // so it is seen as the next token. space = item.length; - item = item.replace(/^ *([*+-]|\d+\.) +/, ''); + item = item.replace(/^ *([*+-]|\d+\.) */, ''); // Outdent whatever the // list item contains. Hacky. @@ -359,9 +359,10 @@ Lexer.prototype.token = function(src, top) { // Determine whether the next list item belongs here. // Backpedal if it does not belong in this list. - if (this.options.smartLists && i !== l - 1) { + if (i !== l - 1) { b = block.bullet.exec(cap[i + 1])[0]; - if (bull !== b && !(bull.length > 1 && b.length > 1)) { + if (bull.length > 1 ? b.length === 1 + : (b.length > 1 || (this.options.smartLists && b !== bull))) { src = cap.slice(i + 1).join('\n') + src; i = l - 1; } @@ -450,12 +451,12 @@ Lexer.prototype.token = function(src, top) { } // table (gfm) - if (top && (cap = this.rules.table.exec(src))) { + if (cap = this.rules.table.exec(src)) { item = { type: 'table', header: splitCells(cap[1].replace(/^ *| *\| *$/g, '')), align: cap[2].replace(/^ *|\| *$/g, '').split(/ *\| */), - cells: cap[3] ? cap[3].replace(/(?: *\| *)?\n$/, '').split('\n') : [] + cells: cap[3] ? cap[3].replace(/\n$/, '').split('\n') : [] }; if (item.header.length === item.align.length) { @@ -544,14 +545,19 @@ var inline = { link: /^!?\[(label)\]\(href(?:\s+(title))?\s*\)/, reflink: /^!?\[(label)\]\[(?!\s*\])((?:\\[\[\]]?|[^\[\]\\])+)\]/, nolink: /^!?\[(?!\s*\])((?:\[[^\[\]]*\]|\\[\[\]]|[^\[\]])*)\](?:\[\])?/, - strong: /^__([^\s])__(?!_)|^\*\*([^\s])\*\*(?!\*)|^__([^\s][\s\S]*?[^\s])__(?!_)|^\*\*([^\s][\s\S]*?[^\s])\*\*(?!\*)/, - em: /^_([^\s_])_(?!_)|^\*([^\s*"<\[])\*(?!\*)|^_([^\s][\s\S]*?[^\s_])_(?!_)|^_([^\s_][\s\S]*?[^\s])_(?!_)|^\*([^\s"<\[][\s\S]*?[^\s*])\*(?!\*)|^\*([^\s*"<\[][\s\S]*?[^\s])\*(?!\*)/, - code: /^(`+)\s*([\s\S]*?[^`]?)\s*\1(?!`)/, + strong: /^__([^\s_])__(?!_)|^\*\*([^\s*])\*\*(?!\*)|^__([^\s][\s\S]*?[^\s])__(?!_)|^\*\*([^\s][\s\S]*?[^\s])\*\*(?!\*)/, + em: /^_([^\s_])_(?!_)|^\*([^\s*"<\[])\*(?!\*)|^_([^\s][\s\S]*?[^\s_])_(?!_|[^\spunctuation])|^_([^\s_][\s\S]*?[^\s])_(?!_|[^\spunctuation])|^\*([^\s"<\[][\s\S]*?[^\s*])\*(?!\*)|^\*([^\s*"<\[][\s\S]*?[^\s])\*(?!\*)/, + code: /^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/, br: /^( {2,}|\\)\n(?!\s*$)/, del: noop, - text: /^[\s\S]+?(?=[\\?@\\[^_{|}~'; +inline.em = edit(inline.em).replace(/punctuation/g, inline._punctuation).getRegex(); + inline._escapes = /\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/g; inline._scheme = /[a-zA-Z][a-zA-Z0-9+.-]{1,31}/; @@ -568,8 +574,8 @@ inline.tag = edit(inline.tag) .replace('attribute', inline._attribute) .getRegex(); -inline._label = /(?:\[[^\[\]]*\]|\\[\[\]]?|`[^`]*`|[^\[\]\\])*?/; -inline._href = /\s*(<(?:\\[<>]?|[^\s<>\\])*>|(?:\\[()]?|\([^\s\x00-\x1f\\]*\)|[^\s\x00-\x1f()\\])*?)/; +inline._label = /(?:\[[^\[\]]*\]|\\[\[\]]?|`[^`]*`|`(?!`)|[^\[\]\\`])*?/; +inline._href = /\s*(<(?:\\[<>]?|[^\s<>\\])*>|[^\s\x00-\x1f]*)/; inline._title = /"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/; inline.link = edit(inline.link) @@ -609,24 +615,23 @@ inline.pedantic = merge({}, inline.normal, { inline.gfm = merge({}, inline.normal, { escape: edit(inline.escape).replace('])', '~|])').getRegex(), - url: edit(/^((?:ftp|https?):\/\/|www\.)(?:[a-zA-Z0-9\-]+\.?)+[^\s<]*|^email/) - .replace('email', inline._email) - .getRegex(), + _extended_email: /[A-Za-z0-9._+-]+(@)[a-zA-Z0-9-_]+(?:\.[a-zA-Z0-9-_]*[a-zA-Z0-9])+(?![-_])/, + url: /^((?:ftp|https?):\/\/|www\.)(?:[a-zA-Z0-9\-]+\.?)+[^\s<]*|^email/, _backpedal: /(?:[^?!.,:;*_~()&]+|\([^)]*\)|&(?![a-zA-Z0-9]+;$)|[?!.,:;*_~)]+(?!$))+/, del: /^~+(?=\S)([\s\S]*?\S)~+/, - text: edit(inline.text) - .replace(']|', '~]|') - .replace('|', '|https?://|ftp://|www\\.|[a-zA-Z0-9.!#$%&\'*+/=?^_`{\\|}~-]+@|') - .getRegex() + text: /^(`+|[^`])(?:[\s\S]*?(?:(?=[\\/i.test(cap[0])) { this.inLink = false; } + if (!this.inRawBlock && /^<(pre|code|kbd|script)(\s|>)/i.test(cap[0])) { + this.inRawBlock = true; + } else if (this.inRawBlock && /^<\/(pre|code|kbd|script)(\s|>)/i.test(cap[0])) { + this.inRawBlock = false; + } + src = src.substring(cap[0].length); out += this.options.sanitize ? this.options.sanitizer ? this.options.sanitizer(cap[0]) : escape(cap[0]) - : cap[0] + : cap[0]; continue; } // link if (cap = this.rules.link.exec(src)) { + var lastParenIndex = findClosingBracket(cap[2], '()'); + if (lastParenIndex > -1) { + var linkLen = cap[0].length - (cap[2].length - lastParenIndex) - (cap[3] || '').length; + cap[2] = cap[2].substring(0, lastParenIndex); + cap[0] = cap[0].substring(0, linkLen).trim(); + cap[3] = ''; + } src = src.substring(cap[0].length); this.inLink = true; href = cap[2]; @@ -821,10 +803,51 @@ InlineLexer.prototype.output = function(src) { continue; } + // autolink + if (cap = this.rules.autolink.exec(src)) { + src = src.substring(cap[0].length); + if (cap[2] === '@') { + text = escape(this.mangle(cap[1])); + href = 'mailto:' + text; + } else { + text = escape(cap[1]); + href = text; + } + out += this.renderer.link(href, null, text); + continue; + } + + // url (gfm) + if (!this.inLink && (cap = this.rules.url.exec(src))) { + if (cap[2] === '@') { + text = escape(cap[0]); + href = 'mailto:' + text; + } else { + // do extended autolink path validation + do { + prevCapZero = cap[0]; + cap[0] = this.rules._backpedal.exec(cap[0])[0]; + } while (prevCapZero !== cap[0]); + text = escape(cap[0]); + if (cap[1] === 'www.') { + href = 'http://' + text; + } else { + href = text; + } + } + src = src.substring(cap[0].length); + out += this.renderer.link(href, null, text); + continue; + } + // text if (cap = this.rules.text.exec(src)) { src = src.substring(cap[0].length); - out += this.renderer.text(escape(this.smartypants(cap[0]))); + if (this.inRawBlock) { + out += this.renderer.text(cap[0]); + } else { + out += this.renderer.text(escape(this.smartypants(cap[0]))); + } continue; } @@ -838,7 +861,7 @@ InlineLexer.prototype.output = function(src) { InlineLexer.escapes = function(text) { return text ? text.replace(InlineLexer.rules._escapes, '$1') : text; -} +}; /** * Compile Link @@ -906,7 +929,8 @@ function Renderer(options) { this.options = options || marked.defaults; } -Renderer.prototype.code = function(code, lang, escaped) { +Renderer.prototype.code = function(code, infostring, escaped) { + var lang = (infostring || '').match(/\S*/)[0]; if (this.options.highlight) { var out = this.options.highlight(code, lang); if (out != null && out !== code) { @@ -937,13 +961,13 @@ Renderer.prototype.html = function(html) { return html; }; -Renderer.prototype.heading = function(text, level, raw) { +Renderer.prototype.heading = function(text, level, raw, slugger) { if (this.options.headerIds) { return '' + text + ' '; -} +}; Renderer.prototype.paragraph = function(text) { return '

' + text + '

\n'; @@ -1025,24 +1049,8 @@ Renderer.prototype.del = function(text) { }; Renderer.prototype.link = function(href, title, text) { - if (this.options.sanitize) { - try { - var prot = decodeURIComponent(unescape(href)) - .replace(/[^\w:]/g, '') - .toLowerCase(); - } catch (e) { - return text; - } - if (prot.indexOf('javascript:') === 0 || prot.indexOf('vbscript:') === 0 || prot.indexOf('data:') === 0) { - return text; - } - } - if (this.options.baseUrl && !originIndependentUrl.test(href)) { - href = resolveUrl(this.options.baseUrl, href); - } - try { - href = encodeURI(href).replace(/%25/g, '%'); - } catch (e) { + href = cleanUrl(this.options.sanitize, this.options.baseUrl, href); + if (href === null) { return text; } var out = '?@[\]^`{|}~]/g, '') + .replace(/\s/g, '-'); + + if (this.seen.hasOwnProperty(slug)) { + var originalSlug = slug; + do { + this.seen[originalSlug]++; + slug = originalSlug + '-' + this.seen[originalSlug]; + } while (this.seen.hasOwnProperty(slug)); + } + this.seen[slug] = 0; + + return slug; +}; + /** * Helpers */ function escape(html, encode) { - return html - .replace(!encode ? /&(?!#?\w+;)/g : /&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, '''); + if (encode) { + if (escape.escapeTest.test(html)) { + return html.replace(escape.escapeReplace, function (ch) { return escape.replacements[ch]; }); + } + } else { + if (escape.escapeTestNoEncode.test(html)) { + return html.replace(escape.escapeReplaceNoEncode, function (ch) { return escape.replacements[ch]; }); + } + } + + return html; } +escape.escapeTest = /[&<>"']/; +escape.escapeReplace = /[&<>"']/g; +escape.replacements = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''' +}; + +escape.escapeTestNoEncode = /[<>"']|&(?!#?\w+;)/; +escape.escapeReplaceNoEncode = /[<>"']|&(?!#?\w+;)/g; + function unescape(html) { // explicitly match decimal, hex, and named HTML entities return html.replace(/&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/ig, function(_, n) { @@ -1316,6 +1386,30 @@ function edit(regex, opt) { }; } +function cleanUrl(sanitize, base, href) { + if (sanitize) { + try { + var prot = decodeURIComponent(unescape(href)) + .replace(/[^\w:]/g, '') + .toLowerCase(); + } catch (e) { + return null; + } + if (prot.indexOf('javascript:') === 0 || prot.indexOf('vbscript:') === 0 || prot.indexOf('data:') === 0) { + return null; + } + } + if (base && !originIndependentUrl.test(href)) { + href = resolveUrl(base, href); + } + try { + href = encodeURI(href).replace(/%25/g, '%'); + } catch (e) { + return null; + } + return href; +} + function resolveUrl(base, href) { if (!baseUrls[' ' + base]) { // we can ignore everything in base after the last slash of its path component, @@ -1418,6 +1512,26 @@ function rtrim(str, c, invert) { return str.substr(0, str.length - suffLen); } +function findClosingBracket(str, b) { + if (str.indexOf(b[1]) === -1) { + return -1; + } + var level = 0; + for (var i = 0; i < str.length; i++) { + if (str[i] === '\\') { + i++; + } else if (str[i] === b[0]) { + level++; + } else if (str[i] === b[1]) { + level--; + if (level < 0) { + return i; + } + } + } + return -1; +} + /** * Marked */ @@ -1446,7 +1560,7 @@ function marked(src, opt, callback) { i = 0; try { - tokens = Lexer.lex(src, opt) + tokens = Lexer.lex(src, opt); } catch (e) { return callback(e); } @@ -1545,7 +1659,7 @@ marked.getDefaults = function () { tables: true, xhtml: false }; -} +}; marked.defaults = marked.getDefaults(); @@ -1565,6 +1679,8 @@ marked.lexer = Lexer.lex; marked.InlineLexer = InlineLexer; marked.inlineLexer = InlineLexer.output; +marked.Slugger = Slugger; + marked.parse = marked; // BEGIN MONACOCHANGE @@ -1582,7 +1698,7 @@ __marked_exports = marked; // ESM-comment-begin define(function() { return __marked_exports; }); // ESM-comment-end - + // ESM-uncomment-begin // export var marked = __marked_exports; // export var Parser = __marked_exports.Parser; diff --git a/src/vs/base/common/resources.ts b/src/vs/base/common/resources.ts index dde4a358fd..d4888386ec 100644 --- a/src/vs/base/common/resources.ts +++ b/src/vs/base/common/resources.ts @@ -146,7 +146,7 @@ export function normalizePath(resource: URI): URI { export function originalFSPath(uri: URI): string { let value: string; const uriPath = uri.path; - if (uri.authority && uriPath.length > 1 && uri.scheme === 'file') { + if (uri.authority && uriPath.length > 1 && uri.scheme === Schemas.file) { // unc path: file://shares/c$/far/boo value = `//${uri.authority}${uriPath}`; } else if ( diff --git a/src/vs/base/parts/ipc/common/ipc.net.ts b/src/vs/base/parts/ipc/common/ipc.net.ts index f799065774..e1a0316d5f 100644 --- a/src/vs/base/parts/ipc/common/ipc.net.ts +++ b/src/vs/base/parts/ipc/common/ipc.net.ts @@ -151,7 +151,7 @@ export const enum ProtocolConstants { /** * If there is no reconnection within this time-frame, consider the connection permanently closed... */ - ReconnectionGraceTime = 60 * 60 * 1000, // 1hr + ReconnectionGraceTime = 3 * 60 * 60 * 1000, // 3hrs } class ProtocolMessage { @@ -687,6 +687,10 @@ export class PersistentProtocol { this._recvKeepAliveCheck(); } + public acceptDisconnect(): void { + this._onClose.fire(); + } + private _receiveMessage(msg: ProtocolMessage): void { if (msg.ack > this._outgoingAckId) { this._outgoingAckId = msg.ack; diff --git a/src/vs/code/electron-main/window.ts b/src/vs/code/electron-main/window.ts index f2ae8517da..7c682fe74e 100644 --- a/src/vs/code/electron-main/window.ts +++ b/src/vs/code/electron-main/window.ts @@ -557,8 +557,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { const configuration = configurationIn ? configurationIn : objects.mixin({}, this.currentConfig); // Delete some properties we do not want during reload - delete configuration.filesToOpen; - delete configuration.filesToCreate; + delete configuration.filesToOpenOrCreate; delete configuration.filesToDiff; delete configuration.filesToWait; diff --git a/src/vs/code/electron-main/windows.ts b/src/vs/code/electron-main/windows.ts index dd75ecd411..9159e6e935 100644 --- a/src/vs/code/electron-main/windows.ts +++ b/src/vs/code/electron-main/windows.ts @@ -91,8 +91,7 @@ interface IPathParseOptions { } interface IFileInputs { - filesToOpen: IPath[]; - filesToCreate: IPath[]; + filesToOpenOrCreate: IPath[]; filesToDiff: IPath[]; filesToWait?: IPathsToWaitFor; remoteAuthority?: string; @@ -112,9 +111,6 @@ interface IPathToOpen extends IPath { // the remote authority for the Code instance to open. Undefined if not remote. remoteAuthority?: string; - // indicator to create the file path in the Code instance - createFilePath?: boolean; - // optional label for the recent history label?: string; } @@ -397,13 +393,9 @@ export class WindowsManager implements IWindowsMainService { workspacesToOpen.push(path); } else if (path.fileUri) { if (!fileInputs) { - fileInputs = { filesToCreate: [], filesToOpen: [], filesToDiff: [], remoteAuthority: path.remoteAuthority }; - } - if (!path.createFilePath) { - fileInputs.filesToOpen.push(path); - } else { - fileInputs.filesToCreate.push(path); + fileInputs = { filesToOpenOrCreate: [], filesToDiff: [], remoteAuthority: path.remoteAuthority }; } + fileInputs.filesToOpenOrCreate.push(path); } else if (path.backupPath) { emptyToRestore.push({ backupFolder: basename(path.backupPath), remoteAuthority: path.remoteAuthority }); } else { @@ -413,15 +405,14 @@ export class WindowsManager implements IWindowsMainService { // When run with --diff, take the files to open as files to diff // if there are exactly two files provided. - if (fileInputs && openConfig.diffMode && fileInputs.filesToOpen.length === 2) { - fileInputs.filesToDiff = fileInputs.filesToOpen; - fileInputs.filesToOpen = []; - fileInputs.filesToCreate = []; // diff ignores other files that do not exist + if (fileInputs && openConfig.diffMode && fileInputs.filesToOpenOrCreate.length === 2) { + fileInputs.filesToDiff = fileInputs.filesToOpenOrCreate; + fileInputs.filesToOpenOrCreate = []; } // When run with --wait, make sure we keep the paths to wait for if (fileInputs && openConfig.waitMarkerFileURI) { - fileInputs.filesToWait = { paths: [...fileInputs.filesToDiff, ...fileInputs.filesToOpen, ...fileInputs.filesToCreate], waitMarkerFileUri: openConfig.waitMarkerFileURI }; + fileInputs.filesToWait = { paths: [...fileInputs.filesToDiff, ...fileInputs.filesToOpenOrCreate], waitMarkerFileUri: openConfig.waitMarkerFileURI }; } // @@ -551,7 +542,7 @@ export class WindowsManager implements IWindowsMainService { if (potentialWindowsCount === 0 && fileInputs) { // Find suitable window or folder path to open files in - const fileToCheck = fileInputs.filesToOpen[0] || fileInputs.filesToCreate[0] || fileInputs.filesToDiff[0]; + const fileToCheck = fileInputs.filesToOpenOrCreate[0] || fileInputs.filesToDiff[0]; // only look at the windows with correct authority const windows = WindowsManager.WINDOWS.filter(w => w.remoteAuthority === fileInputs!.remoteAuthority); @@ -746,10 +737,9 @@ export class WindowsManager implements IWindowsMainService { private doOpenFilesInExistingWindow(configuration: IOpenConfiguration, window: ICodeWindow, fileInputs?: IFileInputs): ICodeWindow { window.focus(); // make sure window has focus - const params: { filesToOpen?: IPath[], filesToCreate?: IPath[], filesToDiff?: IPath[], filesToWait?: IPathsToWaitFor, termProgram?: string } = {}; + const params: { filesToOpenOrCreate?: IPath[], filesToDiff?: IPath[], filesToWait?: IPathsToWaitFor, termProgram?: string } = {}; if (fileInputs) { - params.filesToOpen = fileInputs.filesToOpen; - params.filesToCreate = fileInputs.filesToCreate; + params.filesToOpenOrCreate = fileInputs.filesToOpenOrCreate; params.filesToDiff = fileInputs.filesToDiff; params.filesToWait = fileInputs.filesToWait; } @@ -958,7 +948,7 @@ export class WindowsManager implements IWindowsMainService { if (pathToOpen && pathToOpen.folderUri) { windowsToOpen.push(pathToOpen); } - } else if (restoreWindows !== 'folders' && openedWindow.backupPath) { // Windows that were Empty + } else if (restoreWindows !== 'folders' && openedWindow.backupPath && !openedWindow.remoteAuthority) { // Local windows that were empty. Empty windows with backups will always be restored in open() windowsToOpen.push({ backupPath: openedWindow.backupPath, remoteAuthority: openedWindow.remoteAuthority }); } } @@ -1065,47 +1055,57 @@ export class WindowsManager implements IWindowsMainService { anyPath = parsedPath.path; } - // open remote if either specified in the cli even if it is a local file. TODO: Future idea: resolve in remote host context. + // open remote if either specified in the cli even if it is a local file. TODO@aeschli: Future idea: resolve in remote host context. const remoteAuthority = options.remoteAuthority; const candidate = normalize(anyPath); try { const candidateStat = fs.statSync(candidate); - if (candidateStat) { - if (candidateStat.isFile()) { + if (candidateStat.isFile()) { - // Workspace (unless disabled via flag) - if (!forceOpenWorkspaceAsFile) { - const workspace = this.workspacesMainService.resolveLocalWorkspaceSync(URI.file(candidate)); - if (workspace) { - return { workspace: { id: workspace.id, configPath: workspace.configPath }, remoteAuthority: workspace.remoteAuthority }; - } + // Workspace (unless disabled via flag) + if (!forceOpenWorkspaceAsFile) { + const workspace = this.workspacesMainService.resolveLocalWorkspaceSync(URI.file(candidate)); + if (workspace) { + return { + workspace: { id: workspace.id, configPath: workspace.configPath }, + remoteAuthority: workspace.remoteAuthority, + exists: true + }; } - - // File - return { - fileUri: URI.file(candidate), - lineNumber, - columnNumber, - remoteAuthority - }; } - // Folder (we check for isDirectory() because e.g. paths like /dev/null - // are neither file nor folder but some external tools might pass them - // over to us) - else if (candidateStat.isDirectory()) { - return { - folderUri: URI.file(candidate), - remoteAuthority - }; - } + // File + return { + fileUri: URI.file(candidate), + lineNumber, + columnNumber, + remoteAuthority, + exists: true + }; + } + + // Folder (we check for isDirectory() because e.g. paths like /dev/null + // are neither file nor folder but some external tools might pass them + // over to us) + else if (candidateStat.isDirectory()) { + return { + folderUri: URI.file(candidate), + remoteAuthority, + exists: true + }; } } catch (error) { const fileUri = URI.file(candidate); this.historyMainService.removeFromRecentlyOpened([fileUri]); // since file does not seem to exist anymore, remove from recent + + // assume this is a file that does not yet exist if (options && options.ignoreFileNotFound) { - return { fileUri, createFilePath: true, remoteAuthority }; // assume this is a file that does not yet exist + return { + fileUri, + remoteAuthority, + exists: false + }; } } @@ -1279,8 +1279,7 @@ export class WindowsManager implements IWindowsMainService { const fileInputs = options.fileInputs; if (fileInputs) { - configuration.filesToOpen = fileInputs.filesToOpen; - configuration.filesToCreate = fileInputs.filesToCreate; + configuration.filesToOpenOrCreate = fileInputs.filesToOpenOrCreate; configuration.filesToDiff = fileInputs.filesToDiff; configuration.filesToWait = fileInputs.filesToWait; } diff --git a/src/vs/platform/editor/common/editor.ts b/src/vs/platform/editor/common/editor.ts index 0273098c28..4995e98882 100644 --- a/src/vs/platform/editor/common/editor.ts +++ b/src/vs/platform/editor/common/editor.ts @@ -49,6 +49,15 @@ export interface IBaseResourceInput { * looking at the scheme of the resource(s). */ readonly forceFile?: boolean; + + /** + * Hint to indicate that this input should be treated as a + * untitled file. + * + * Without this hint, the editor service will make a guess by + * looking at the scheme of the resource(s). + */ + readonly forceUntitled?: boolean; } export interface IResourceInput extends IBaseResourceInput { diff --git a/src/vs/platform/files/common/files.ts b/src/vs/platform/files/common/files.ts index 2e4491c599..051f19e146 100644 --- a/src/vs/platform/files/common/files.ts +++ b/src/vs/platform/files/common/files.ts @@ -288,7 +288,12 @@ export function markAsFileSystemProviderError(error: Error, code: FileSystemProv return error; } -export function toFileSystemProviderErrorCode(error: Error): FileSystemProviderErrorCode { +export function toFileSystemProviderErrorCode(error: Error | undefined | null): FileSystemProviderErrorCode { + + // Guard against abuse + if (!error) { + return FileSystemProviderErrorCode.Unknown; + } // FileSystemProviderError comes with the code if (error instanceof FileSystemProviderError) { @@ -758,12 +763,12 @@ export const FALLBACK_MAX_MEMORY_SIZE_MB = 4096; */ export const ETAG_DISABLED = ''; -export function etag(mtime: number, size: number): string; -export function etag(mtime: number | undefined, size: number | undefined): string | undefined; -export function etag(mtime: number | undefined, size: number | undefined): string | undefined { - if (typeof size !== 'number' || typeof mtime !== 'number') { +export function etag(stat: { mtime: number, size: number }): string; +export function etag(stat: { mtime: number | undefined, size: number | undefined }): string | undefined; +export function etag(stat: { mtime: number | undefined, size: number | undefined }): string | undefined { + if (typeof stat.size !== 'number' || typeof stat.mtime !== 'number') { return undefined; } - return mtime.toString(29) + size.toString(31); + return stat.mtime.toString(29) + stat.size.toString(31); } diff --git a/src/vs/platform/update/node/update.config.contribution.ts b/src/vs/platform/update/node/update.config.contribution.ts index 6905c4407e..9956898dbb 100644 --- a/src/vs/platform/update/node/update.config.contribution.ts +++ b/src/vs/platform/update/node/update.config.contribution.ts @@ -44,6 +44,7 @@ configurationRegistry.registerConfiguration({ 'update.showReleaseNotes': { type: 'boolean', default: true, + scope: ConfigurationScope.APPLICATION, description: localize('showReleaseNotes', "Show Release Notes after an update. The Release Notes are fetched from a Microsoft online service."), tags: ['usesOnlineServices'] } diff --git a/src/vs/platform/windows/common/windows.ts b/src/vs/platform/windows/common/windows.ts index 328530a4e0..c5118746f0 100644 --- a/src/vs/platform/windows/common/windows.ts +++ b/src/vs/platform/windows/common/windows.ts @@ -358,7 +358,7 @@ export const enum ReadyState { export interface IPath extends IPathData { - // the file path to open within a Code instance + // the file path to open within the instance fileUri?: URI; } @@ -374,7 +374,7 @@ export interface IPathsToWaitForData { export interface IPathData { - // the file path to open within a Code instance + // the file path to open within the instance fileUri?: UriComponents; // the line number in the file path to open @@ -382,11 +382,15 @@ export interface IPathData { // the column number in the file path to open columnNumber?: number; + + // a hint that the file exists. if true, the + // file exists, if false it does not. with + // undefined the state is unknown. + exists?: boolean; } export interface IOpenFileRequest { - filesToOpen?: IPathData[]; - filesToCreate?: IPathData[]; + filesToOpenOrCreate?: IPathData[]; filesToDiff?: IPathData[]; filesToWait?: IPathsToWaitForData; termProgram?: string; @@ -430,8 +434,7 @@ export interface IWindowConfiguration extends ParsedArgs { perfWindowLoadTime?: number; perfEntries: ExportData; - filesToOpen?: IPath[]; - filesToCreate?: IPath[]; + filesToOpenOrCreate?: IPath[]; filesToDiff?: IPath[]; filesToWait?: IPathsToWaitFor; termProgram?: string; diff --git a/src/vs/platform/windows/electron-browser/windowsService.ts b/src/vs/platform/windows/electron-browser/windowsService.ts index f160cfddb5..e7cc17dcf1 100644 --- a/src/vs/platform/windows/electron-browser/windowsService.ts +++ b/src/vs/platform/windows/electron-browser/windowsService.ts @@ -74,9 +74,11 @@ export class WindowsService implements IWindowsService { return this.channel.call('closeWorkspace', windowId); } - enterWorkspace(windowId: number, path: URI): Promise { + enterWorkspace(windowId: number, path: URI): Promise { return this.channel.call('enterWorkspace', [windowId, path]).then((result: IEnterWorkspaceResult) => { - result.workspace = reviveWorkspaceIdentifier(result.workspace); + if (result) { + result.workspace = reviveWorkspaceIdentifier(result.workspace); + } return result; }); } diff --git a/src/vs/platform/windows/electron-main/windowsService.ts b/src/vs/platform/windows/electron-main/windowsService.ts index 1d532f2a1e..b0140617f5 100644 --- a/src/vs/platform/windows/electron-main/windowsService.ts +++ b/src/vs/platform/windows/electron-main/windowsService.ts @@ -176,7 +176,7 @@ export class WindowsService implements IWindowsService, IURLHandler, IDisposable async getRecentlyOpened(windowId: number): Promise { this.logService.trace('windowsService#getRecentlyOpened', windowId); - return this.withWindow(windowId, codeWindow => this.historyService.getRecentlyOpened(codeWindow.config.workspace, codeWindow.config.folderUri, codeWindow.config.filesToOpen), () => this.historyService.getRecentlyOpened())!; + return this.withWindow(windowId, codeWindow => this.historyService.getRecentlyOpened(codeWindow.config.workspace, codeWindow.config.folderUri, codeWindow.config.filesToOpenOrCreate), () => this.historyService.getRecentlyOpened())!; } async newWindowTab(): Promise { diff --git a/src/vs/platform/workspaces/common/workspaces.ts b/src/vs/platform/workspaces/common/workspaces.ts index 84ec1af6c2..a7898ab9f0 100644 --- a/src/vs/platform/workspaces/common/workspaces.ts +++ b/src/vs/platform/workspaces/common/workspaces.ts @@ -10,7 +10,7 @@ import { IWorkspaceFolder, IWorkspace } from 'vs/platform/workspace/common/works import { URI, UriComponents } from 'vs/base/common/uri'; import { isWindows, isLinux, isMacintosh } from 'vs/base/common/platform'; import { extname } from 'vs/base/common/path'; -import { dirname, resolvePath, isEqualAuthority, isEqualOrParent, relativePath } from 'vs/base/common/resources'; +import { dirname, resolvePath, isEqualAuthority, isEqualOrParent, relativePath, extname as resourceExtname } from 'vs/base/common/resources'; import * as jsonEdit from 'vs/base/common/jsonEdit'; import * as json from 'vs/base/common/json'; import { Schemas } from 'vs/base/common/network'; @@ -158,8 +158,10 @@ export function isSingleFolderWorkspaceInitializationPayload(obj: any): obj is I const WORKSPACE_SUFFIX = '.' + WORKSPACE_EXTENSION; -export function hasWorkspaceFileExtension(path: string) { - return extname(path) === WORKSPACE_SUFFIX; +export function hasWorkspaceFileExtension(path: string | URI) { + const ext = (typeof path === 'string') ? extname(path) : resourceExtname(path); + + return ext === WORKSPACE_SUFFIX; } const SLASH = '/'; diff --git a/src/vs/platform/workspaces/electron-main/workspacesMainService.ts b/src/vs/platform/workspaces/electron-main/workspacesMainService.ts index f0c6f54270..1e4af393ec 100644 --- a/src/vs/platform/workspaces/electron-main/workspacesMainService.ts +++ b/src/vs/platform/workspaces/electron-main/workspacesMainService.ts @@ -61,7 +61,7 @@ export class WorkspacesMainService extends Disposable implements IWorkspacesMain } private isWorkspacePath(uri: URI): boolean { - return this.isInsideWorkspacesHome(uri) || hasWorkspaceFileExtension(uri.path); + return this.isInsideWorkspacesHome(uri) || hasWorkspaceFileExtension(uri); } private doResolveWorkspace(path: URI, contents: string): IResolvedWorkspace | null { diff --git a/src/vs/workbench/api/common/apiCommands.ts b/src/vs/workbench/api/common/apiCommands.ts index 3083ad95a3..af71f02cdb 100644 --- a/src/vs/workbench/api/common/apiCommands.ts +++ b/src/vs/workbench/api/common/apiCommands.ts @@ -36,6 +36,7 @@ function adjustHandler(handler: (executor: ICommandsExecutor, ...args: any[]) => interface IOpenFolderAPICommandOptions { forceNewWindow?: boolean; + forceReuseWindow?: boolean; noRecentEntry?: boolean; } @@ -50,9 +51,9 @@ export class OpenFolderAPICommand { if (!uri) { return executor.executeCommand('_files.pickFolderAndOpen', { forceNewWindow: arg.forceNewWindow }); } - const options: IOpenSettings = { forceNewWindow: arg.forceNewWindow, noRecentEntry: arg.noRecentEntry }; + const options: IOpenSettings = { forceNewWindow: arg.forceNewWindow, forceReuseWindow: arg.forceReuseWindow, noRecentEntry: arg.noRecentEntry }; uri = URI.revive(uri); - const uriToOpen: IURIToOpen = (hasWorkspaceFileExtension(uri.path) || uri.scheme === Schemas.untitled) ? { workspaceUri: uri } : { folderUri: uri }; + const uriToOpen: IURIToOpen = (hasWorkspaceFileExtension(uri) || uri.scheme === Schemas.untitled) ? { workspaceUri: uri } : { folderUri: uri }; return executor.executeCommand('_files.windowOpen', [uriToOpen], options); } } diff --git a/src/vs/workbench/browser/dnd.ts b/src/vs/workbench/browser/dnd.ts index d586c10196..f0e18376df 100644 --- a/src/vs/workbench/browser/dnd.ts +++ b/src/vs/workbench/browser/dnd.ts @@ -260,7 +260,7 @@ export class ResourcesDropHandler { return Promise.all(fileOnDiskResources.map(fileOnDiskResource => { // Check for Workspace - if (hasWorkspaceFileExtension(fileOnDiskResource.fsPath)) { + if (hasWorkspaceFileExtension(fileOnDiskResource)) { urisToOpen.push({ workspaceUri: fileOnDiskResource }); return undefined; diff --git a/src/vs/workbench/browser/layout.ts b/src/vs/workbench/browser/layout.ts index f0de12726b..8c59677c0d 100644 --- a/src/vs/workbench/browser/layout.ts +++ b/src/vs/workbench/browser/layout.ts @@ -10,8 +10,7 @@ import { onDidChangeFullscreen, isFullscreen, getZoomFactor } from 'vs/base/brow import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; import { Registry } from 'vs/platform/registry/common/platform'; import { isWindows, isLinux, isMacintosh } from 'vs/base/common/platform'; -import { IResourceInput } from 'vs/platform/editor/common/editor'; -import { IUntitledResourceInput, IResourceDiffInput } from 'vs/workbench/common/editor'; +import { IUntitledResourceInput, pathsToEditors } from 'vs/workbench/common/editor'; import { SidebarPart } from 'vs/workbench/browser/parts/sidebar/sidebarPart'; import { PanelPart } from 'vs/workbench/browser/parts/panel/panelPart'; import { PanelRegistry, Extensions as PanelExtensions } from 'vs/workbench/browser/panel'; @@ -24,7 +23,7 @@ import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; import { ITitleService } from 'vs/workbench/services/title/common/titleService'; import { IInstantiationService, ServicesAccessor, ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation'; import { LifecyclePhase, StartupKind, ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; -import { IWindowService, IPath, MenuBarVisibility, getTitleBarStyle } from 'vs/platform/windows/common/windows'; +import { IWindowService, MenuBarVisibility, getTitleBarStyle } from 'vs/platform/windows/common/windows'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IEditorService, IResourceEditor } from 'vs/workbench/services/editor/common/editorService'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; @@ -34,7 +33,7 @@ import { IDimension } from 'vs/platform/layout/browser/layoutService'; import { Part } from 'vs/workbench/browser/part'; import { IStatusbarService } from 'vs/platform/statusbar/common/statusbar'; import { IActivityBarService } from 'vs/workbench/services/activityBar/browser/activityBarService'; -import { coalesce } from 'vs/base/common/arrays'; +import { IFileService } from 'vs/platform/files/common/files'; enum Settings { MENUBAR_VISIBLE = 'window.menuBarVisibility', @@ -182,7 +181,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi this.registerLayoutListeners(); // State - this.initLayoutState(accessor.get(ILifecycleService)); + this.initLayoutState(accessor.get(ILifecycleService), accessor.get(IFileService)); } private registerLayoutListeners(): void { @@ -319,7 +318,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi } } - private initLayoutState(lifecycleService: ILifecycleService): void { + private initLayoutState(lifecycleService: ILifecycleService, fileService: IFileService): void { // Fullscreen this.state.fullscreen = isFullscreen(); @@ -358,7 +357,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi this.state.editor.restoreCentered = this.storageService.getBoolean(Storage.CENTERED_LAYOUT_ENABLED, StorageScope.WORKSPACE, false); // Editors to open - this.state.editor.editorsToOpen = this.resolveEditorsToOpen(); + this.state.editor.editorsToOpen = this.resolveEditorsToOpen(fileService); // Panel visibility this.state.panel.hidden = this.storageService.getBoolean(Storage.PANEL_HIDDEN, StorageScope.WORKSPACE, true); @@ -389,7 +388,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi this.state.zenMode.restore = this.storageService.getBoolean(Storage.ZEN_MODE_ENABLED, StorageScope.WORKSPACE, false) && this.configurationService.getValue(Settings.ZEN_MODE_RESTORE); } - private resolveEditorsToOpen(): Promise | IResourceEditor[] { + private resolveEditorsToOpen(fileService: IFileService): Promise | IResourceEditor[] { const configuration = this.environmentService.configuration; const hasInitialFilesToOpen = this.hasInitialFilesToOpen(); @@ -400,21 +399,19 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi if (hasInitialFilesToOpen) { // Files to diff is exclusive - const filesToDiff = this.toInputs(configuration.filesToDiff, false); - if (filesToDiff && filesToDiff.length === 2) { - return [{ - leftResource: filesToDiff[0].resource, - rightResource: filesToDiff[1].resource, - options: { pinned: true }, - forceFile: true - }]; - } + return pathsToEditors(configuration.filesToDiff, fileService).then(filesToDiff => { + if (filesToDiff && filesToDiff.length === 2) { + return [{ + leftResource: filesToDiff[0].resource, + rightResource: filesToDiff[1].resource, + options: { pinned: true }, + forceFile: true + }]; + } - const filesToCreate = this.toInputs(configuration.filesToCreate, true); - const filesToOpen = this.toInputs(configuration.filesToOpen, false); - - // Otherwise: Open/Create files - return [...filesToOpen, ...filesToCreate]; + // Otherwise: Open/Create files + return pathsToEditors(configuration.filesToOpenOrCreate, fileService); + }); } // Empty workbench @@ -439,38 +436,9 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi const configuration = this.environmentService.configuration; return !!( - (configuration.filesToCreate && configuration.filesToCreate.length > 0) || - (configuration.filesToOpen && configuration.filesToOpen.length > 0) || - (configuration.filesToDiff && configuration.filesToDiff.length > 0)); - } - - private toInputs(paths: IPath[] | undefined, isNew: boolean): Array { - if (!paths || !paths.length) { - return []; - } - - return coalesce(paths.map(p => { - const resource = p.fileUri; - if (!resource) { - return undefined; - } - - let input: IResourceInput | IUntitledResourceInput; - if (isNew) { - input = { filePath: resource.fsPath, options: { pinned: true } }; - } else { - input = { resource, options: { pinned: true }, forceFile: true }; - } - - if (!isNew && typeof p.lineNumber === 'number') { - input.options!.selection = { - startLineNumber: p.lineNumber, - startColumn: p.columnNumber || 1 - }; - } - - return input; - })); + (configuration.filesToOpenOrCreate && configuration.filesToOpenOrCreate.length > 0) || + (configuration.filesToDiff && configuration.filesToDiff.length > 0) + ); } private updatePanelPosition() { diff --git a/src/vs/workbench/browser/nodeless.simpleservices.ts b/src/vs/workbench/browser/nodeless.simpleservices.ts index a661f4e2ea..6db31aa25c 100644 --- a/src/vs/workbench/browser/nodeless.simpleservices.ts +++ b/src/vs/workbench/browser/nodeless.simpleservices.ts @@ -953,8 +953,7 @@ export class SimpleWindowConfiguration implements IWindowConfiguration { perfWindowLoadTime?: number; perfEntries: ExportData; - filesToOpen?: IPath[]; - filesToCreate?: IPath[]; + filesToOpenOrCreate?: IPath[]; filesToDiff?: IPath[]; filesToWait?: IPathsToWaitFor; termProgram?: string; diff --git a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts index ee97ec5168..5f3b186cb4 100644 --- a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts +++ b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts @@ -76,7 +76,7 @@ export class ActivitybarPart extends Part implements IActivityBarService { @IStorageService private readonly storageService: IStorageService, @IExtensionService private readonly extensionService: IExtensionService, @IViewsService private readonly viewsService: IViewsService, - @IContextKeyService private readonly contextKeyService: IContextKeyService, + @IContextKeyService private readonly contextKeyService: IContextKeyService ) { super(Parts.ACTIVITYBAR_PART, { hasTitle: false }, themeService, storageService, layoutService); @@ -150,7 +150,7 @@ export class ActivitybarPart extends Part implements IActivityBarService { if (viewContainer && viewContainer.hideIfEmpty) { const viewDescriptors = this.viewsService.getViewDescriptors(viewContainer); if (viewDescriptors && viewDescriptors.activeViewDescriptors.length === 0) { - this.removeComposite(viewletDescriptor.id, true); // Update the composite bar by hiding + this.hideComposite(viewletDescriptor.id); // Update the composite bar by hiding } } } @@ -309,14 +309,14 @@ export class ActivitybarPart extends Part implements IActivityBarService { disposable.dispose(); } this.viewletDisposables.delete(viewletId); - this.removeComposite(viewletId, true); + this.hideComposite(viewletId); } private onDidChangeActiveViews(viewlet: ViewletDescriptor, viewDescriptors: IViewDescriptorCollection): void { if (viewDescriptors.activeViewDescriptors.length) { this.compositeBar.addComposite(viewlet); } else { - this.removeComposite(viewlet.id, true); + this.hideComposite(viewlet.id); } } @@ -334,18 +334,13 @@ export class ActivitybarPart extends Part implements IActivityBarService { const viewlets = this.viewletService.getViewlets(); for (const { id } of this.cachedViewlets) { if (viewlets.every(viewlet => viewlet.id !== id)) { - this.removeComposite(id, false); + this.hideComposite(id); } } } - private removeComposite(compositeId: string, hide: boolean): void { - if (hide) { - this.compositeBar.hideComposite(compositeId); - } else { - this.compositeBar.removeComposite(compositeId); - } - + private hideComposite(compositeId: string): void { + this.compositeBar.hideComposite(compositeId); const compositeActions = this.compositeActions[compositeId]; if (compositeActions) { compositeActions.activityAction.dispose(); @@ -441,7 +436,9 @@ export class ActivitybarPart extends Part implements IActivityBarService { } } } - state.push({ id: compositeItem.id, iconUrl: viewlet.iconUrl && viewlet.iconUrl.scheme === Schemas.file ? viewlet.iconUrl : undefined, views, pinned: compositeItem && compositeItem.pinned, order: compositeItem ? compositeItem.order : undefined, visible: compositeItem && compositeItem.visible }); + state.push({ id: compositeItem.id, iconUrl: viewlet.iconUrl && viewlet.iconUrl.scheme === Schemas.file ? viewlet.iconUrl : undefined, views, pinned: compositeItem.pinned, order: compositeItem.order, visible: compositeItem.visible }); + } else { + state.push({ id: compositeItem.id, pinned: compositeItem.pinned, order: compositeItem.order, visible: false }); } } diff --git a/src/vs/workbench/browser/parts/editor/binaryEditor.ts b/src/vs/workbench/browser/parts/editor/binaryEditor.ts index 115f1f45b3..fb7703439e 100644 --- a/src/vs/workbench/browser/parts/editor/binaryEditor.ts +++ b/src/vs/workbench/browser/parts/editor/binaryEditor.ts @@ -19,6 +19,7 @@ import { ITextFileService } from 'vs/workbench/services/textfile/common/textfile import { CancellationToken } from 'vs/base/common/cancellation'; import { dispose } from 'vs/base/common/lifecycle'; import { IStorageService } from 'vs/platform/storage/common/storage'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; export interface IOpenCallbacks { openInternal: (input: EditorInput, options: EditorOptions) => Promise; @@ -48,6 +49,7 @@ export abstract class BaseBinaryResourceEditor extends BaseEditor { telemetryService: ITelemetryService, themeService: IThemeService, @ITextFileService private readonly textFileService: ITextFileService, + @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, @IStorageService storageService: IStorageService ) { super(id, telemetryService, themeService, storageService); @@ -92,9 +94,11 @@ export abstract class BaseBinaryResourceEditor extends BaseEditor { this.textFileService, this.binaryContainer, this.scrollbar, - resource => this.handleOpenInternalCallback(input, options), - resource => this.callbacks.openExternal(resource), - meta => this.handleMetadataChanged(meta) + { + openInternalClb: _ => this.handleOpenInternalCallback(input, options), + openExternalClb: this.environmentService.configuration.remoteAuthority ? undefined : resource => this.callbacks.openExternal(resource), + metadataClb: meta => this.handleMetadataChanged(meta) + } ); return undefined; diff --git a/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts b/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts index c25e3c8e08..e9ecda5eae 100644 --- a/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts +++ b/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts @@ -47,6 +47,7 @@ import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService import { IEditorGroupView } from 'vs/workbench/browser/parts/editor/editor'; import { onDidChangeZoomLevel } from 'vs/base/browser/browser'; import { withNullAsUndefined, withUndefinedAsNull } from 'vs/base/common/types'; +import { ILabelService } from 'vs/platform/label/common/label'; class Item extends BreadcrumbsItem { @@ -167,6 +168,7 @@ export class BreadcrumbsControl { @IConfigurationService private readonly _configurationService: IConfigurationService, @IFileService private readonly _fileService: IFileService, @ITelemetryService private readonly _telemetryService: ITelemetryService, + @ILabelService private readonly _labelService: ILabelService, @IBreadcrumbsService breadcrumbsService: IBreadcrumbsService, ) { this.domNode = document.createElement('div'); @@ -238,16 +240,18 @@ export class BreadcrumbsControl { this._ckBreadcrumbsVisible.set(true); this._ckBreadcrumbsPossible.set(true); - let editor = this._getActiveCodeEditor(); - let model = new EditorBreadcrumbsModel(input.getResource()!, editor, this._workspaceService, this._configurationService); + const uri = input.getResource()!; + const editor = this._getActiveCodeEditor(); + const model = new EditorBreadcrumbsModel(uri, editor, this._workspaceService, this._configurationService); dom.toggleClass(this.domNode, 'relative-path', model.isRelative()); + dom.toggleClass(this.domNode, 'backslash-path', this._labelService.getSeparator(uri.scheme, uri.authority) === '\\'); - let updateBreadcrumbs = () => { - let items = model.getElements().map(element => new Item(element, this._options, this._instantiationService)); + const updateBreadcrumbs = () => { + const items = model.getElements().map(element => new Item(element, this._options, this._instantiationService)); this._widget.setItems(items); this._widget.reveal(items[items.length - 1]); }; - let listener = model.onDidUpdate(updateBreadcrumbs); + const listener = model.onDidUpdate(updateBreadcrumbs); updateBreadcrumbs(); this._breadcrumbsDisposables = [model, listener]; diff --git a/src/vs/workbench/browser/parts/editor/editor.contribution.ts b/src/vs/workbench/browser/parts/editor/editor.contribution.ts index 884cd78691..ca335f65e1 100644 --- a/src/vs/workbench/browser/parts/editor/editor.contribution.ts +++ b/src/vs/workbench/browser/parts/editor/editor.contribution.ts @@ -47,7 +47,6 @@ import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/co import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { isMacintosh } from 'vs/base/common/platform'; import { AllEditorsPicker, ActiveEditorGroupPicker } from 'vs/workbench/browser/parts/editor/editorPicker'; -import { Schemas } from 'vs/base/common/network'; import { registerEditorContribution } from 'vs/editor/browser/editorExtensions'; import { OpenWorkspaceButtonContribution } from 'vs/workbench/browser/parts/editor/editorWidgets'; import { ZoomStatusbarItem } from 'vs/workbench/browser/parts/editor/resourceViewer'; @@ -148,11 +147,10 @@ class UntitledEditorInputFactory implements IEditorInputFactory { return instantiationService.invokeFunction(accessor => { const deserialized: ISerializedUntitledEditorInput = JSON.parse(serializedEditorInput); const resource = !!deserialized.resourceJSON ? URI.revive(deserialized.resourceJSON) : URI.parse(deserialized.resource); - const filePath = resource.scheme === Schemas.untitled ? undefined : resource.scheme === Schemas.file ? resource.fsPath : resource.path; const language = deserialized.modeId; const encoding = deserialized.encoding; - return accessor.get(IEditorService).createInput({ resource, filePath, language, encoding }) as UntitledEditorInput; + return accessor.get(IEditorService).createInput({ resource, language, encoding, forceUntitled: true }) as UntitledEditorInput; }); } } diff --git a/src/vs/workbench/browser/parts/editor/editorGroupView.ts b/src/vs/workbench/browser/parts/editor/editorGroupView.ts index 68e29d985e..9fa7efad88 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupView.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupView.ts @@ -51,7 +51,8 @@ import { IVisibleEditor } from 'vs/workbench/services/editor/common/editorServic import { withNullAsUndefined } from 'vs/base/common/types'; import { hash } from 'vs/base/common/hash'; import { guessMimeTypes } from 'vs/base/common/mime'; -import { extname } from 'vs/base/common/path'; +import { extname } from 'vs/base/common/resources'; +import { Schemas } from 'vs/base/common/network'; export class EditorGroupView extends Themable implements IEditorGroupView { @@ -526,8 +527,9 @@ export class EditorGroupView extends Themable implements IEditorGroupView { const descriptor = editor.getTelemetryDescriptor(); const resource = editor.getResource(); - if (resource && resource.fsPath) { - descriptor['resource'] = { mimeType: guessMimeTypes(resource.fsPath).join(', '), scheme: resource.scheme, ext: extname(resource.fsPath), path: hash(resource.fsPath) }; + const path = resource ? resource.scheme === Schemas.file ? resource.fsPath : resource.path : undefined; + if (resource && path) { + descriptor['resource'] = { mimeType: guessMimeTypes(path).join(', '), scheme: resource.scheme, ext: extname(resource), path: hash(path) }; /* __GDPR__FRAGMENT__ "EditorTelemetryDescriptor" : { diff --git a/src/vs/workbench/browser/parts/editor/editorWidgets.ts b/src/vs/workbench/browser/parts/editor/editorWidgets.ts index 17825d8555..9238d3a726 100644 --- a/src/vs/workbench/browser/parts/editor/editorWidgets.ts +++ b/src/vs/workbench/browser/parts/editor/editorWidgets.ts @@ -139,7 +139,7 @@ export class OpenWorkspaceButtonContribution extends Disposable implements IEdit return false; // we need a model } - if (!hasWorkspaceFileExtension(model.uri.fsPath)) { + if (!hasWorkspaceFileExtension(model.uri)) { return false; // we need a workspace file } diff --git a/src/vs/workbench/browser/parts/editor/media/notabstitlecontrol.css b/src/vs/workbench/browser/parts/editor/media/notabstitlecontrol.css index 34176cb720..d505c304cd 100644 --- a/src/vs/workbench/browser/parts/editor/media/notabstitlecontrol.css +++ b/src/vs/workbench/browser/parts/editor/media/notabstitlecontrol.css @@ -54,7 +54,7 @@ background-image: none; } -.monaco-workbench.windows .part.editor > .content .editor-group-container > .title.breadcrumbs .breadcrumbs-control .monaco-breadcrumb-item::before { +.monaco-workbench .part.editor > .content .editor-group-container > .title.breadcrumbs .breadcrumbs-control.backslash-path .monaco-breadcrumb-item::before { content: '\\'; } diff --git a/src/vs/workbench/browser/parts/editor/resourceViewer.ts b/src/vs/workbench/browser/parts/editor/resourceViewer.ts index 7a71f93428..a022d952e2 100644 --- a/src/vs/workbench/browser/parts/editor/resourceViewer.ts +++ b/src/vs/workbench/browser/parts/editor/resourceViewer.ts @@ -62,6 +62,12 @@ export interface ResourceViewerContext extends IDisposable { layout?(dimension: DOM.Dimension): void; } +interface ResourceViewerDelegate { + openInternalClb(uri: URI): void; + openExternalClb?(uri: URI): void; + metadataClb(meta: string): void; +} + /** * Helper to actually render the given resource into the provided container. Will adjust scrollbar (if provided) automatically based on loading * progress of the binary resource. @@ -75,9 +81,7 @@ export class ResourceViewer { textFileService: ITextFileService, container: HTMLElement, scrollbar: DomScrollableElement, - openInternalClb: (uri: URI) => void, - openExternalClb: (uri: URI) => void, - metadataClb: (meta: string) => void + delegate: ResourceViewerDelegate ): ResourceViewerContext { // Ensure CSS class @@ -85,17 +89,17 @@ export class ResourceViewer { // Images if (ResourceViewer.isImageResource(descriptor)) { - return ImageView.create(container, descriptor, textFileService, scrollbar, openExternalClb, metadataClb); + return ImageView.create(container, descriptor, textFileService, scrollbar, delegate); } // Large Files if (descriptor.size > ResourceViewer.MAX_OPEN_INTERNAL_SIZE) { - return FileTooLargeFileView.create(container, descriptor, scrollbar, metadataClb); + return FileTooLargeFileView.create(container, descriptor, scrollbar, delegate); } // Seemingly Binary Files else { - return FileSeemsBinaryFileView.create(container, descriptor, scrollbar, openInternalClb, metadataClb); + return FileSeemsBinaryFileView.create(container, descriptor, scrollbar, delegate); } } @@ -116,14 +120,13 @@ class ImageView { descriptor: IResourceDescriptor, textFileService: ITextFileService, scrollbar: DomScrollableElement, - openExternalClb: (uri: URI) => void, - metadataClb: (meta: string) => void + delegate: ResourceViewerDelegate ): ResourceViewerContext { if (ImageView.shouldShowImageInline(descriptor)) { - return InlineImageView.create(container, descriptor, textFileService, scrollbar, metadataClb); + return InlineImageView.create(container, descriptor, textFileService, scrollbar, delegate); } - return LargeImageView.create(container, descriptor, openExternalClb, metadataClb); + return LargeImageView.create(container, descriptor, delegate); } private static shouldShowImageInline(descriptor: IResourceDescriptor): boolean { @@ -150,11 +153,10 @@ class LargeImageView { static create( container: HTMLElement, descriptor: IResourceDescriptor, - openExternalClb: (uri: URI) => void, - metadataClb: (meta: string) => void + delegate: ResourceViewerDelegate ) { const size = BinarySize.formatSize(descriptor.size); - metadataClb(size); + delegate.metadataClb(size); DOM.clearNode(container); @@ -164,12 +166,13 @@ class LargeImageView { label.textContent = nls.localize('largeImageError', "The image is not displayed in the editor because it is too large ({0}).", size); container.appendChild(label); - if (descriptor.resource.scheme === Schemas.file) { + const openExternal = delegate.openExternalClb; + if (descriptor.resource.scheme === Schemas.file && openExternal) { const link = DOM.append(label, DOM.$('a.embedded-link')); link.setAttribute('role', 'button'); link.textContent = nls.localize('resourceOpenExternalButton', "Open image using external program?"); - disposables.push(DOM.addDisposableListener(link, DOM.EventType.CLICK, () => openExternalClb(descriptor.resource))); + disposables.push(DOM.addDisposableListener(link, DOM.EventType.CLICK, () => openExternal(descriptor.resource))); } return combinedDisposable(disposables); @@ -181,10 +184,10 @@ class FileTooLargeFileView { container: HTMLElement, descriptor: IResourceDescriptor, scrollbar: DomScrollableElement, - metadataClb: (meta: string) => void + delegate: ResourceViewerDelegate ) { const size = BinarySize.formatSize(descriptor.size); - metadataClb(size); + delegate.metadataClb(size); DOM.clearNode(container); @@ -203,10 +206,9 @@ class FileSeemsBinaryFileView { container: HTMLElement, descriptor: IResourceDescriptor, scrollbar: DomScrollableElement, - openInternalClb: (uri: URI) => void, - metadataClb: (meta: string) => void + delegate: ResourceViewerDelegate ) { - metadataClb(typeof descriptor.size === 'number' ? BinarySize.formatSize(descriptor.size) : ''); + delegate.metadataClb(typeof descriptor.size === 'number' ? BinarySize.formatSize(descriptor.size) : ''); DOM.clearNode(container); @@ -221,7 +223,7 @@ class FileSeemsBinaryFileView { link.setAttribute('role', 'button'); link.textContent = nls.localize('openAsText', "Do you want to open it anyway?"); - disposables.push(DOM.addDisposableListener(link, DOM.EventType.CLICK, () => openInternalClb(descriptor.resource))); + disposables.push(DOM.addDisposableListener(link, DOM.EventType.CLICK, () => delegate.openInternalClb(descriptor.resource))); } scrollbar.scanDomNode(); @@ -359,7 +361,7 @@ class InlineImageView { descriptor: IResourceDescriptor, textFileService: ITextFileService, scrollbar: DomScrollableElement, - metadataClb: (meta: string) => void + delegate: ResourceViewerDelegate ) { const disposables: IDisposable[] = []; @@ -543,9 +545,9 @@ class InlineImageView { return; } if (typeof descriptor.size === 'number') { - metadataClb(nls.localize('imgMeta', '{0}x{1} {2}', image.naturalWidth, image.naturalHeight, BinarySize.formatSize(descriptor.size))); + delegate.metadataClb(nls.localize('imgMeta', '{0}x{1} {2}', image.naturalWidth, image.naturalHeight, BinarySize.formatSize(descriptor.size))); } else { - metadataClb(nls.localize('imgMetaNoSize', '{0}x{1}', image.naturalWidth, image.naturalHeight)); + delegate.metadataClb(nls.localize('imgMetaNoSize', '{0}x{1}', image.naturalWidth, image.naturalHeight)); } scrollbar.scanDomNode(); diff --git a/src/vs/workbench/browser/parts/quickinput/quickInput.ts b/src/vs/workbench/browser/parts/quickinput/quickInput.ts index 5e53918fc9..bf7077e124 100644 --- a/src/vs/workbench/browser/parts/quickinput/quickInput.ts +++ b/src/vs/workbench/browser/parts/quickinput/quickInput.ts @@ -255,7 +255,7 @@ class QuickInput implements IQuickInput { this.ui.leftActionBar.clear(); const leftButtons = this.buttons.filter(button => button === backButton); this.ui.leftActionBar.push(leftButtons.map((button, index) => { - const action = new Action(`id-${index}`, '', button.iconClass || getIconClass(button.iconPath!), true, () => { + const action = new Action(`id-${index}`, '', button.iconClass || getIconClass(button.iconPath), true, () => { this.onDidTriggerButtonEmitter.fire(button); return Promise.resolve(null); }); @@ -265,7 +265,7 @@ class QuickInput implements IQuickInput { this.ui.rightActionBar.clear(); const rightButtons = this.buttons.filter(button => button !== backButton); this.ui.rightActionBar.push(rightButtons.map((button, index) => { - const action = new Action(`id-${index}`, '', button.iconClass || getIconClass(button.iconPath!), true, () => { + const action = new Action(`id-${index}`, '', button.iconClass || getIconClass(button.iconPath), true, () => { this.onDidTriggerButtonEmitter.fire(button); return Promise.resolve(null); }); @@ -757,7 +757,7 @@ class QuickPick extends QuickInput implements IQuickPi this.showMessageDecoration(Severity.Error); } else { this.ui.message.textContent = null; - this.showMessageDecoration(Severity.Info); + this.showMessageDecoration(Severity.Ignore); } this.ui.customButton.label = this.customLabel; this.ui.customButton.element.title = this.customHover; @@ -888,7 +888,7 @@ class InputBox extends QuickInput implements IInputBox { } if (!this.validationMessage && this.ui.message.textContent !== this.noValidationMessage) { this.ui.message.textContent = this.noValidationMessage; - this.showMessageDecoration(Severity.Info); + this.showMessageDecoration(Severity.Ignore); } if (this.validationMessage && this.ui.message.textContent !== this.validationMessage) { this.ui.message.textContent = this.validationMessage; @@ -1571,4 +1571,4 @@ export class BackAction extends Action { } } -registerSingleton(IQuickInputService, QuickInputService, true); \ No newline at end of file +registerSingleton(IQuickInputService, QuickInputService, true); diff --git a/src/vs/workbench/browser/parts/quickinput/quickInputUtils.ts b/src/vs/workbench/browser/parts/quickinput/quickInputUtils.ts index 34093d1c88..371651f119 100644 --- a/src/vs/workbench/browser/parts/quickinput/quickInputUtils.ts +++ b/src/vs/workbench/browser/parts/quickinput/quickInputUtils.ts @@ -11,7 +11,10 @@ import { IdGenerator } from 'vs/base/common/idGenerator'; const iconPathToClass = {}; const iconClassGenerator = new IdGenerator('quick-input-button-icon-'); -export function getIconClass(iconPath: { dark: URI; light?: URI; }) { +export function getIconClass(iconPath: { dark: URI; light?: URI; } | undefined): string | undefined { + if (!iconPath) { + return undefined; + } let iconClass: string; const key = iconPath.dark.toString(); diff --git a/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts index f869dcce7d..8e8d7805cd 100644 --- a/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts +++ b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts @@ -36,6 +36,7 @@ import { IStorageService } from 'vs/platform/storage/common/storage'; import { Parts, IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; import { RunOnceScheduler } from 'vs/base/common/async'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { Schemas } from 'vs/base/common/network'; export class TitlebarPart extends Part implements ITitleService { @@ -179,7 +180,7 @@ export class TitlebarPart extends Part implements ITitleService { } private updateRepresentedFilename(): void { - const file = toResource(this.editorService.activeEditor, { supportSideBySide: SideBySideEditor.MASTER, filterByScheme: 'file' }); + const file = toResource(this.editorService.activeEditor, { supportSideBySide: SideBySideEditor.MASTER, filterByScheme: Schemas.file }); const path = file ? file.fsPath : ''; // Apply to window diff --git a/src/vs/workbench/common/editor.ts b/src/vs/workbench/common/editor.ts index 419c4fa1ba..abb91f3dda 100644 --- a/src/vs/workbench/common/editor.ts +++ b/src/vs/workbench/common/editor.ts @@ -9,7 +9,7 @@ import { isUndefinedOrNull, withUndefinedAsNull } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import { IDisposable, Disposable } from 'vs/base/common/lifecycle'; import { IEditor as ICodeEditor, IEditorViewState, ScrollType, IDiffEditor } from 'vs/editor/common/editorCommon'; -import { IEditorModel, IEditorOptions, ITextEditorOptions, IBaseResourceInput } from 'vs/platform/editor/common/editor'; +import { IEditorModel, IEditorOptions, ITextEditorOptions, IBaseResourceInput, IResourceInput } from 'vs/platform/editor/common/editor'; import { IInstantiationService, IConstructorSignature0, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { RawContextKey, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { Registry } from 'vs/platform/registry/common/platform'; @@ -17,6 +17,9 @@ import { ITextModel } from 'vs/editor/common/model'; import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; import { ICompositeControl } from 'vs/workbench/common/composite'; import { ActionRunner, IAction } from 'vs/base/common/actions'; +import { IFileService } from 'vs/platform/files/common/files'; +import { IPathData } from 'vs/platform/windows/common/windows'; +import { coalesce } from 'vs/base/common/arrays'; export const ActiveEditorContext = new RawContextKey('activeEditor', null); export const EditorsVisibleContext = new RawContextKey('editorIsOpen', false); @@ -198,15 +201,11 @@ export interface IEditorInputFactory { export interface IUntitledResourceInput extends IBaseResourceInput { /** - * Optional resource. If the resource is not provided a new untitled file is created. + * Optional resource. If the resource is not provided a new untitled file is created (e.g. Untitled-1). + * Otherwise the untitled editor will have an associated path and use that when saving. */ resource?: URI; - /** - * Optional file path. Using the file resource will associate the file to the untitled resource. - */ - filePath?: string; - /** * Optional language of the untitled resource. */ @@ -1089,3 +1088,37 @@ export const Extensions = { }; Registry.add(Extensions.EditorInputFactories, new EditorInputFactoryRegistry()); + +export async function pathsToEditors(paths: IPathData[] | undefined, fileService: IFileService): Promise<(IResourceInput | IUntitledResourceInput)[]> { + if (!paths || !paths.length) { + return []; + } + + const editors = await Promise.all(paths.map(async path => { + const resource = URI.revive(path.fileUri); + if (!resource || !fileService.canHandleResource(resource)) { + return undefined; // {{SQL CARBON EDIT}} @anthonydresser revert after strictnullchecks + } + + const exists = (typeof path.exists === 'boolean') ? path.exists : await fileService.exists(resource); + + const options: ITextEditorOptions = { pinned: true }; + if (exists && typeof path.lineNumber === 'number') { + options.selection = { + startLineNumber: path.lineNumber, + startColumn: path.columnNumber || 1 + }; + } + + let input: IResourceInput | IUntitledResourceInput; + if (!exists) { + input = { resource, options, forceUntitled: true }; + } else { + input = { resource, options, forceFile: true }; + } + + return input; + })); + + return coalesce(editors); +} diff --git a/src/vs/workbench/common/theme.ts b/src/vs/workbench/common/theme.ts index 1569b5dc42..531fe4d7e1 100644 --- a/src/vs/workbench/common/theme.ts +++ b/src/vs/workbench/common/theme.ts @@ -310,15 +310,15 @@ export const STATUS_BAR_PROMINENT_ITEM_HOVER_BACKGROUND = registerColor('statusB }, nls.localize('statusBarProminentItemHoverBackground', "Status bar prominent items background color when hovering. Prominent items stand out from other status bar entries to indicate importance. Change mode `Toggle Tab Key Moves Focus` from command palette to see an example. The status bar is shown in the bottom of the window.")); export const STATUS_BAR_HOST_NAME_BACKGROUND = registerColor('statusBarItem.hostBackground', { - dark: STATUS_BAR_PROMINENT_ITEM_BACKGROUND, - light: STATUS_BAR_PROMINENT_ITEM_BACKGROUND, - hc: STATUS_BAR_PROMINENT_ITEM_BACKGROUND + dark: '#C40057', + light: '#C40057', + hc: '#C40057' }, nls.localize('statusBarItemHostBackground', "Background color for the host indicator on the status bar.")); export const STATUS_BAR_HOST_NAME_FOREGROUND = registerColor('statusBarItem.hostForeground', { - dark: STATUS_BAR_PROMINENT_ITEM_FOREGROUND, - light: STATUS_BAR_PROMINENT_ITEM_FOREGROUND, - hc: STATUS_BAR_PROMINENT_ITEM_FOREGROUND + dark: '#FFFFFF', + light: '#FFFFFF', + hc: '#FFFFFF' }, nls.localize('statusBarItemHostForeground', "Foreground color for the host indicator on the status bar.")); diff --git a/src/vs/workbench/contrib/backup/common/backupRestorer.ts b/src/vs/workbench/contrib/backup/common/backupRestorer.ts index 007d12c506..99662104d3 100644 --- a/src/vs/workbench/contrib/backup/common/backupRestorer.ts +++ b/src/vs/workbench/contrib/backup/common/backupRestorer.ts @@ -11,6 +11,8 @@ import { IResourceInput } from 'vs/platform/editor/common/editor'; import { Schemas } from 'vs/base/common/network'; import { ILifecycleService, LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; import { IUntitledResourceInput } from 'vs/workbench/common/editor'; +import { toLocalResource } from 'vs/base/common/resources'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; export class BackupRestorer implements IWorkbenchContribution { @@ -21,7 +23,8 @@ export class BackupRestorer implements IWorkbenchContribution { constructor( @IEditorService private readonly editorService: IEditorService, @IBackupFileService private readonly backupFileService: IBackupFileService, - @ILifecycleService private readonly lifecycleService: ILifecycleService + @ILifecycleService private readonly lifecycleService: ILifecycleService, + @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService ) { this.restoreBackups(); } @@ -79,9 +82,9 @@ export class BackupRestorer implements IWorkbenchContribution { if (resource.scheme === Schemas.untitled && !BackupRestorer.UNTITLED_REGEX.test(resource.fsPath) && !BackupRestorer.SQLQUERY_REGEX.test(resource.fsPath)) { - return { filePath: resource.fsPath, options }; + return { resource: toLocalResource(resource, this.environmentService.configuration.remoteAuthority), options, forceUntitled: true }; } return { resource, options }; } -} \ No newline at end of file +} diff --git a/src/vs/workbench/contrib/extensions/common/extensions.ts b/src/vs/workbench/contrib/extensions/common/extensions.ts index 77050d40e8..06e89f078e 100644 --- a/src/vs/workbench/contrib/extensions/common/extensions.ts +++ b/src/vs/workbench/contrib/extensions/common/extensions.ts @@ -85,6 +85,7 @@ export interface IExtensionsWorkbenchService { _serviceBrand: any; onChange: Event; local: IExtension[]; + installed: IExtension[]; outdated: IExtension[]; queryLocal(server?: IExtensionManagementServer): Promise; queryGallery(token: CancellationToken): Promise>; diff --git a/src/vs/workbench/contrib/extensions/electron-browser/extensionEditor.ts b/src/vs/workbench/contrib/extensions/electron-browser/extensionEditor.ts index 7f6f31331a..aafff0590f 100644 --- a/src/vs/workbench/contrib/extensions/electron-browser/extensionEditor.ts +++ b/src/vs/workbench/contrib/extensions/electron-browser/extensionEditor.ts @@ -28,7 +28,7 @@ import { IExtensionsWorkbenchService, IExtensionsViewlet, VIEWLET_ID, IExtension import { RatingsWidget, InstallCountWidget, RemoteBadgeWidget } from 'vs/workbench/contrib/extensions/electron-browser/extensionsWidgets'; import { EditorOptions } from 'vs/workbench/common/editor'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; -import { CombinedInstallAction, UpdateAction, ExtensionEditorDropDownAction, ReloadAction, MaliciousStatusLabelAction, IgnoreExtensionRecommendationAction, UndoIgnoreExtensionRecommendationAction, EnableDropDownAction, DisableDropDownAction, StatusLabelAction, SetFileIconThemeAction, SetColorThemeAction, RemoteInstallAction, DisabledLabelAction, SystemDisabledWarningAction } from 'vs/workbench/contrib/extensions/electron-browser/extensionsActions'; +import { CombinedInstallAction, UpdateAction, ExtensionEditorDropDownAction, ReloadAction, MaliciousStatusLabelAction, IgnoreExtensionRecommendationAction, UndoIgnoreExtensionRecommendationAction, EnableDropDownAction, DisableDropDownAction, StatusLabelAction, SetFileIconThemeAction, SetColorThemeAction, RemoteInstallAction, DisabledLabelAction, SystemDisabledWarningAction, LocalInstallAction } from 'vs/workbench/contrib/extensions/electron-browser/extensionsActions'; import { WebviewElement } from 'vs/workbench/contrib/webview/electron-browser/webviewElement'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; @@ -300,7 +300,7 @@ export class ExtensionEditor extends BaseEditor { this.extensionManifest = new Cache(() => createCancelablePromise(token => extension.getManifest(token))); this.extensionDependencies = new Cache(() => createCancelablePromise(token => this.extensionsWorkbenchService.loadDependencies(extension, token))); - const remoteBadge = this.instantiationService.createInstance(RemoteBadgeWidget, this.iconContainer); + const remoteBadge = this.instantiationService.createInstance(RemoteBadgeWidget, this.iconContainer, true); const onError = Event.once(domEvent(this.icon, 'error')); onError(() => this.icon.src = extension.iconUrlFallback, null, this.transientDisposables); this.icon.src = extension.iconUrl; @@ -393,6 +393,7 @@ export class ExtensionEditor extends BaseEditor { this.instantiationService.createInstance(EnableDropDownAction), this.instantiationService.createInstance(DisableDropDownAction, runningExtensions), this.instantiationService.createInstance(RemoteInstallAction), + this.instantiationService.createInstance(LocalInstallAction), combinedInstallAction, systemDisabledWarningAction, this.instantiationService.createInstance(DisabledLabelAction, systemDisabledWarningAction), diff --git a/src/vs/workbench/contrib/extensions/electron-browser/extensionsActions.ts b/src/vs/workbench/contrib/extensions/electron-browser/extensionsActions.ts index 3da3e630b4..3a774fd4bb 100644 --- a/src/vs/workbench/contrib/extensions/electron-browser/extensionsActions.ts +++ b/src/vs/workbench/contrib/extensions/electron-browser/extensionsActions.ts @@ -316,12 +316,14 @@ export class RemoteInstallAction extends ExtensionAction { private updateLabel(): void { if (this.installing) { this.label = RemoteInstallAction.INSTALLING_LABEL; + this.tooltip = this.label; return; } const remoteAuthority = this.environmentService.configuration.remoteAuthority; if (remoteAuthority) { const host = this.labelService.getHostLabel(REMOTE_HOST_SCHEME, this.environmentService.configuration.remoteAuthority) || localize('remote', "Remote"); this.label = `${RemoteInstallAction.INSTALL_LABEL} on ${host}`; + this.tooltip = this.label; return; } } @@ -339,9 +341,9 @@ export class RemoteInstallAction extends ExtensionAction { // Installed User Extension && this.extension && this.extension.local && this.extension.type === ExtensionType.User && this.extension.state === ExtensionState.Installed // Local Workspace Extension - && this.extension.server === this.extensionManagementServerService.localExtensionManagementServer && !isUIExtension(this.extension.local.manifest, this.configurationService) + && this.extension.server === this.extensionManagementServerService.localExtensionManagementServer && (isLanguagePackExtension(this.extension.local.manifest) || !isUIExtension(this.extension.local.manifest, this.configurationService)) // Extension does not exist in remote - && !this.extensionsWorkbenchService.local.some(e => areSameExtensions(e.identifier, this.extension.identifier) && e.server === this.extensionManagementServerService.remoteExtensionManagementServer) + && !this.extensionsWorkbenchService.installed.some(e => areSameExtensions(e.identifier, this.extension.identifier) && e.server === this.extensionManagementServerService.remoteExtensionManagementServer) && this.extensionsWorkbenchService.canInstall(this.extension) ) { this.enabled = true; @@ -370,6 +372,85 @@ export class RemoteInstallAction extends ExtensionAction { } } +export class LocalInstallAction extends ExtensionAction { + + private static INSTALL_LABEL = localize('install locally', "Install Locally"); + private static INSTALLING_LABEL = localize('installing', "Installing"); + + private static readonly Class = 'extension-action prominent install'; + private static readonly InstallingClass = 'extension-action install installing'; + + updateWhenCounterExtensionChanges: boolean = true; + private disposables: IDisposable[] = []; + private installing: boolean = false; + + constructor( + @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, + @ILabelService private readonly labelService: ILabelService, + @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, + @IExtensionManagementServerService private readonly extensionManagementServerService: IExtensionManagementServerService, + @IConfigurationService private readonly configurationService: IConfigurationService, + ) { + super(`extensions.localinstall`, LocalInstallAction.INSTALL_LABEL, LocalInstallAction.Class, false); + this.labelService.onDidChangeFormatters(() => this.updateLabel(), this, this.disposables); + this.updateLabel(); + this.update(); + } + + private updateLabel(): void { + if (this.installing) { + this.label = LocalInstallAction.INSTALLING_LABEL; + this.tooltip = this.label; + return; + } + this.label = `${LocalInstallAction.INSTALL_LABEL}`; + this.tooltip = this.label; + } + + update(): void { + this.enabled = false; + this.class = LocalInstallAction.Class; + if (this.installing) { + this.enabled = true; + this.class = LocalInstallAction.InstallingClass; + this.updateLabel(); + return; + } + if (this.environmentService.configuration.remoteAuthority + // Installed User Extension + && this.extension && this.extension.local && this.extension.type === ExtensionType.User && this.extension.state === ExtensionState.Installed + // Remote UI or Language pack Extension + && this.extension.server === this.extensionManagementServerService.remoteExtensionManagementServer && (isLanguagePackExtension(this.extension.local.manifest) || isUIExtension(this.extension.local.manifest, this.configurationService)) + // Extension does not exist in local + && !this.extensionsWorkbenchService.installed.some(e => areSameExtensions(e.identifier, this.extension.identifier) && e.server === this.extensionManagementServerService.localExtensionManagementServer) + && this.extensionsWorkbenchService.canInstall(this.extension) + ) { + this.enabled = true; + this.updateLabel(); + return; + } + } + + async run(): Promise { + if (!this.installing) { + this.installing = true; + this.update(); + this.extensionsWorkbenchService.open(this.extension); + alert(localize('installExtensionStart', "Installing extension {0} started. An editor is now open with more details on this extension", this.extension.displayName)); + if (this.extension.gallery) { + await this.extensionManagementServerService.localExtensionManagementServer.extensionManagementService.installFromGallery(this.extension.gallery); + this.installing = false; + this.update(); + } + } + } + + dispose(): void { + this.disposables = dispose(this.disposables); + super.dispose(); + } +} + export class UninstallAction extends ExtensionAction { private static readonly UninstallLabel = localize('uninstallAction', "Uninstall"); @@ -1265,23 +1346,34 @@ export class ReloadAction extends ExtensionAction { if (isEnabled && !this.extensionService.canAddExtension(toExtensionDescription(this.extension.local))) { this.enabled = true; this.label = localize('reloadRequired', "Reload Required"); - // {{SQL CARBON EDIT}} - replace Visual Studio Code with Azure Data Studio - this.tooltip = localize('postEnableTooltip', "Please reload Azure Data Studio to enable this extension."); + this.tooltip = localize('postEnableTooltip', "Please reload Azure Data Studio to enable this extension."); // {{SQL CARBON EDIT}} - replace Visual Studio Code with Azure Data Studio return; } - if (this.workbenchEnvironmentService.configuration.remoteAuthority + if (this.workbenchEnvironmentService.configuration.remoteAuthority) { + const uiExtension = isUIExtension(this.extension.local.manifest, this.configurationService); // Local Workspace Extension - && this.extension.server === this.extensionManagementServerService.localExtensionManagementServer && !isUIExtension(this.extension.local.manifest, this.configurationService) - ) { - const remoteExtension = this.extensionsWorkbenchService.local.filter(e => areSameExtensions(e.identifier, this.extension.identifier) && e.server === this.extensionManagementServerService.remoteExtensionManagementServer)[0]; - // Extension exist in remote and enabled - if (remoteExtension && remoteExtension.local && this.extensionEnablementService.isEnabled(remoteExtension.local)) { - this.enabled = true; - this.label = localize('reloadRequired', "Reload Required"); - // {{SQL CARBON EDIT}} - replace Visual Studio Code with Azure Data Studio - this.tooltip = localize('postEnableTooltip', "Please reload Azure Data Studio to enable this extension."); - alert(localize('installExtensionComplete', "Installing extension {0} is completed. Please reload Azure Data Studio to enable it.", this.extension.displayName)); - return; + if (!uiExtension && this.extension.server === this.extensionManagementServerService.localExtensionManagementServer) { + const remoteExtension = this.extensionsWorkbenchService.local.filter(e => areSameExtensions(e.identifier, this.extension.identifier) && e.server === this.extensionManagementServerService.remoteExtensionManagementServer)[0]; + // Extension exist in remote and enabled + if (remoteExtension && remoteExtension.local && this.extensionEnablementService.isEnabled(remoteExtension.local)) { + this.enabled = true; + this.label = localize('reloadRequired', "Reload Required"); + this.tooltip = localize('postEnableTooltip', "Please reload Azure Data Studio to enable this extension.");// {{SQL CARBON EDIT}} - replace Visual Studio Code with Azure Data Studio + alert(localize('installExtensionComplete', "Installing extension {0} is completed. Please reload Azure Data Studio to enable it.", this.extension.displayName)); // {{SQL CARBON EDIT}} - replace Visual Studio Code with Azure Data Studio + return; + } + } + // Remote UI Extension + if (uiExtension && this.extension.server === this.extensionManagementServerService.remoteExtensionManagementServer) { + const localExtension = this.extensionsWorkbenchService.local.filter(e => areSameExtensions(e.identifier, this.extension.identifier) && e.server === this.extensionManagementServerService.localExtensionManagementServer)[0]; + // Extension exist in local and enabled + if (localExtension && localExtension.local && this.extensionEnablementService.isEnabled(localExtension.local)) { + this.enabled = true; + this.label = localize('reloadRequired', "Reload Required"); + this.tooltip = localize('postEnableTooltip', "Please reload Azure Data Studio to enable this extension."); // {{SQL CARBON EDIT}} - replace Visual Studio Code with Azure Data Studio + alert(localize('installExtensionComplete', "Installing extension {0} is completed. Please reload Azure Data Studio to enable it.", this.extension.displayName)); // {{SQL CARBON EDIT}} - replace Visual Studio Code with Azure Data Studio + return; + } } } } @@ -2464,7 +2556,8 @@ export class StatusLabelAction extends Action implements IExtensionContainer { } constructor( - @IExtensionService private readonly extensionService: IExtensionService + @IExtensionService private readonly extensionService: IExtensionService, + @IExtensionManagementServerService private readonly extensionManagementServerService: IExtensionManagementServerService ) { super('extensions.action.statusLabel', '', StatusLabelAction.DISABLED_CLASS, false); } @@ -2503,7 +2596,7 @@ export class StatusLabelAction extends Action implements IExtensionContainer { }; const canRemoveExtension = () => { if (this.extension.local) { - if (runningExtensions.every(e => !areSameExtensions({ id: e.identifier.value }, this.extension.identifier))) { + if (runningExtensions.every(e => !(areSameExtensions({ id: e.identifier.value }, this.extension.identifier) && this.extension.server === this.extensionManagementServerService.getExtensionManagementServer(e.extensionLocation)))) { return true; } return this.extensionService.canRemoveExtension(toExtensionDescription(this.extension.local)); @@ -2592,21 +2685,18 @@ export class DisabledLabelAction extends ExtensionAction { update(): void { this.class = `${DisabledLabelAction.Class} hide`; this.label = ''; - this.enabled = false; - if (this.extension && this.extension.local && isLanguagePackExtension(this.extension.local.manifest)) { - return; - } - if (this.warningAction.enabled) { - this.enabled = true; + if (this.warningAction.tooltip) { this.class = DisabledLabelAction.Class; this.label = this.warningAction.tooltip; return; } + if (this.extension && this.extension.local && isLanguagePackExtension(this.extension.local.manifest)) { + return; + } if (this.extension && this.extension.local && this._runningExtensions) { const isEnabled = this.extensionEnablementService.isEnabled(this.extension.local); const isExtensionRunning = this._runningExtensions.some(e => areSameExtensions({ id: e.identifier.value }, this.extension.identifier)); - if (!isExtensionRunning && !isEnabled) { - this.enabled = true; + if (!isExtensionRunning && !isEnabled && this.extensionEnablementService.canChangeEnablement(this.extension.local)) { this.class = DisabledLabelAction.Class; this.label = localize('disabled by user', "This extension is disabled by the user."); return; @@ -2626,7 +2716,9 @@ export class DisabledLabelAction extends ExtensionAction { export class SystemDisabledWarningAction extends ExtensionAction { - private static readonly Class = 'disable-warning'; + private static readonly CLASS = 'system-disable'; + private static readonly WARNING_CLASS = `${SystemDisabledWarningAction.CLASS} warning`; + private static readonly INFO_CLASS = `${SystemDisabledWarningAction.CLASS} info`; updateWhenCounterExtensionChanges: boolean = true; private disposables: IDisposable[] = []; @@ -2640,7 +2732,7 @@ export class SystemDisabledWarningAction extends ExtensionAction { @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, @IExtensionService private readonly extensionService: IExtensionService, ) { - super('extensions.install', '', `${SystemDisabledWarningAction.Class} hide`, false); + super('extensions.install', '', `${SystemDisabledWarningAction.CLASS} hide`, false); this.labelService.onDidChangeFormatters(() => this.update(), this, this.disposables); this.extensionService.onDidChangeExtensions(this.updateRunningExtensions, this, this.disposables); this.updateRunningExtensions(); @@ -2652,44 +2744,54 @@ export class SystemDisabledWarningAction extends ExtensionAction { } update(): void { - this.enabled = false; - this.class = `${SystemDisabledWarningAction.Class} hide`; + this.class = `${SystemDisabledWarningAction.CLASS} hide`; this.tooltip = ''; - if (this.extension && this.extension.local && isLanguagePackExtension(this.extension.local.manifest)) { + if ( + !this.extension || + !this.extension.local || + !this.extension.server || + !this._runningExtensions || + !this.workbenchEnvironmentService.configuration.remoteAuthority || + !this.extensionManagementServerService.remoteExtensionManagementServer || + this.extension.state !== ExtensionState.Installed + ) { return; } - if (this.extension && this.extension.local && this.extension.server && this._runningExtensions && this.workbenchEnvironmentService.configuration.remoteAuthority && this.extensionManagementServerService.remoteExtensionManagementServer) { - const runningExtension = this._runningExtensions.filter(e => areSameExtensions({ id: e.identifier.value }, this.extension.identifier))[0]; - const runningExtensionServer = runningExtension ? this.extensionManagementServerService.getExtensionManagementServer(runningExtension.extensionLocation) : null; - const localExtension = this.extensionsWorkbenchService.local.filter(e => areSameExtensions(e.identifier, this.extension.identifier))[0]; - const localExtensionServer = localExtension ? localExtension.server : null; - if (this.extension.server === this.extensionManagementServerService.localExtensionManagementServer && !isUIExtension(this.extension.local.manifest, this.configurationService)) { - if (runningExtensionServer === this.extensionManagementServerService.remoteExtensionManagementServer) { - this.enabled = true; - this.class = `${SystemDisabledWarningAction.Class}`; - this.tooltip = localize('disabled locally', "Extension is enabled on '{0}' and disabled locally.", this.getServerLabel(this.extensionManagementServerService.remoteExtensionManagementServer)); - return; - } - if (localExtensionServer !== this.extensionManagementServerService.remoteExtensionManagementServer) { - this.enabled = true; - this.class = `${SystemDisabledWarningAction.Class}`; - this.tooltip = localize('Install in remote server', "Install the extension on '{0}' to enable.", this.getServerLabel(this.extensionManagementServerService.remoteExtensionManagementServer)); - return; - } + if (isLanguagePackExtension(this.extension.local.manifest)) { + if (!this.extensionsWorkbenchService.installed.some(e => areSameExtensions(e.identifier, this.extension.identifier) && e.server !== this.extension.server)) { + this.class = `${SystemDisabledWarningAction.INFO_CLASS}`; + this.tooltip = this.extension.server === this.extensionManagementServerService.localExtensionManagementServer + ? localize('Install language pack also in remote server', "Install the language pack extension on '{0}' to enable it also there.", this.getServerLabel(this.extensionManagementServerService.remoteExtensionManagementServer)) + : localize('Install language pack also locally', "Install the language pack extension locally to enable it also there."); } - if (this.extension.server === this.extensionManagementServerService.remoteExtensionManagementServer && isUIExtension(this.extension.local.manifest, this.configurationService)) { - if (runningExtensionServer === this.extensionManagementServerService.localExtensionManagementServer) { - this.enabled = true; - this.class = `${SystemDisabledWarningAction.Class}`; - this.tooltip = localize('disabled remotely', "Extension is enabled locally and disabled on '{0}'.", this.getServerLabel(this.extensionManagementServerService.remoteExtensionManagementServer)); - return; - } - if (localExtensionServer !== this.extensionManagementServerService.localExtensionManagementServer) { - this.enabled = true; - this.class = `${SystemDisabledWarningAction.Class}`; - this.tooltip = localize('Install in local server', "Install the extension locally to enable."); - return; - } + return; + } + const runningExtension = this._runningExtensions.filter(e => areSameExtensions({ id: e.identifier.value }, this.extension.identifier))[0]; + const runningExtensionServer = runningExtension ? this.extensionManagementServerService.getExtensionManagementServer(runningExtension.extensionLocation) : null; + const localExtension = this.extensionsWorkbenchService.local.filter(e => areSameExtensions(e.identifier, this.extension.identifier))[0]; + const localExtensionServer = localExtension ? localExtension.server : null; + if (this.extension.server === this.extensionManagementServerService.localExtensionManagementServer && !isUIExtension(this.extension.local.manifest, this.configurationService)) { + if (runningExtensionServer === this.extensionManagementServerService.remoteExtensionManagementServer) { + this.class = `${SystemDisabledWarningAction.INFO_CLASS}`; + this.tooltip = localize('disabled locally', "Extension is enabled on '{0}' and disabled locally.", this.getServerLabel(this.extensionManagementServerService.remoteExtensionManagementServer)); + return; + } + if (localExtensionServer !== this.extensionManagementServerService.remoteExtensionManagementServer) { + this.class = `${SystemDisabledWarningAction.WARNING_CLASS}`; + this.tooltip = localize('Install in remote server', "Install the extension on '{0}' to enable.", this.getServerLabel(this.extensionManagementServerService.remoteExtensionManagementServer)); + return; + } + } + if (this.extension.server === this.extensionManagementServerService.remoteExtensionManagementServer && isUIExtension(this.extension.local.manifest, this.configurationService)) { + if (runningExtensionServer === this.extensionManagementServerService.localExtensionManagementServer) { + this.class = `${SystemDisabledWarningAction.INFO_CLASS}`; + this.tooltip = localize('disabled remotely', "Extension is enabled locally and disabled on '{0}'.", this.getServerLabel(this.extensionManagementServerService.remoteExtensionManagementServer)); + return; + } + if (localExtensionServer !== this.extensionManagementServerService.localExtensionManagementServer) { + this.class = `${SystemDisabledWarningAction.WARNING_CLASS}`; + this.tooltip = localize('Install in local server', "Install the extension locally to enable."); + return; } } } diff --git a/src/vs/workbench/contrib/extensions/electron-browser/extensionsList.ts b/src/vs/workbench/contrib/extensions/electron-browser/extensionsList.ts index fd4e30b2f5..7c0e7547bc 100644 --- a/src/vs/workbench/contrib/extensions/electron-browser/extensionsList.ts +++ b/src/vs/workbench/contrib/extensions/electron-browser/extensionsList.ts @@ -13,7 +13,7 @@ import { IPagedRenderer } from 'vs/base/browser/ui/list/listPaging'; import { Event } from 'vs/base/common/event'; import { domEvent } from 'vs/base/browser/event'; import { IExtension, ExtensionContainers, ExtensionState, IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions'; -import { InstallAction, UpdateAction, ManageExtensionAction, ReloadAction, MaliciousStatusLabelAction, ExtensionActionItem, StatusLabelAction, RemoteInstallAction, SystemDisabledWarningAction, DisabledLabelAction } from 'vs/workbench/contrib/extensions/electron-browser/extensionsActions'; +import { InstallAction, UpdateAction, ManageExtensionAction, ReloadAction, MaliciousStatusLabelAction, ExtensionActionItem, StatusLabelAction, RemoteInstallAction, SystemDisabledWarningAction, DisabledLabelAction, LocalInstallAction } from 'vs/workbench/contrib/extensions/electron-browser/extensionsActions'; import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { Label, RatingsWidget, InstallCountWidget, RecommendationWidget, RemoteBadgeWidget, TooltipWidget } from 'vs/workbench/contrib/extensions/electron-browser/extensionsWidgets'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; @@ -67,7 +67,7 @@ export class Renderer implements IPagedRenderer { const element = append(root, $('.extension')); const iconContainer = append(element, $('.icon-container')); const icon = append(iconContainer, $('img.icon')); - const iconRemoteBadgeWidget = this.instantiationService.createInstance(RemoteBadgeWidget, iconContainer); + const iconRemoteBadgeWidget = this.instantiationService.createInstance(RemoteBadgeWidget, iconContainer, false); const details = append(element, $('.details')); const headerContainer = append(details, $('.header-container')); const header = append(headerContainer, $('.header')); @@ -75,7 +75,7 @@ export class Renderer implements IPagedRenderer { const version = append(header, $('span.version')); const installCount = append(header, $('span.install-count')); const ratings = append(header, $('span.ratings')); - const headerRemoteBadgeWidget = this.instantiationService.createInstance(RemoteBadgeWidget, header); + const headerRemoteBadgeWidget = this.instantiationService.createInstance(RemoteBadgeWidget, header, false); const description = append(details, $('.description.ellipsis')); const footer = append(details, $('.footer')); const author = append(footer, $('.author.ellipsis')); @@ -98,6 +98,7 @@ export class Renderer implements IPagedRenderer { reloadAction, this.instantiationService.createInstance(InstallAction), this.instantiationService.createInstance(RemoteInstallAction), + this.instantiationService.createInstance(LocalInstallAction), this.instantiationService.createInstance(MaliciousStatusLabelAction, false), systemDisabledWarningAction, this.instantiationService.createInstance(ManageExtensionAction) diff --git a/src/vs/workbench/contrib/extensions/electron-browser/extensionsViewlet.ts b/src/vs/workbench/contrib/extensions/electron-browser/extensionsViewlet.ts index 936c8f01ad..8e2c1b6365 100644 --- a/src/vs/workbench/contrib/extensions/electron-browser/extensionsViewlet.ts +++ b/src/vs/workbench/contrib/extensions/electron-browser/extensionsViewlet.ts @@ -55,6 +55,9 @@ import { ExtensionType } from 'vs/platform/extensions/common/extensions'; import { Registry } from 'vs/platform/registry/common/platform'; import { ViewContainerViewlet } from 'vs/workbench/browser/parts/views/viewsViewlet'; import { RemoteAuthorityContext } from 'vs/workbench/common/contextkeys'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { REMOTE_HOST_SCHEME } from 'vs/platform/remote/common/remoteHosts'; +import { ILabelService } from 'vs/platform/label/common/label'; interface SearchInputEvent extends Event { target: HTMLInputElement; @@ -91,7 +94,9 @@ const viewIdNameMappings: { [id: string]: string } = { export class ExtensionsViewletViewsContribution implements IWorkbenchContribution { constructor( - @IExtensionManagementServerService private readonly extensionManagementServerService: IExtensionManagementServerService + @IExtensionManagementServerService private readonly extensionManagementServerService: IExtensionManagementServerService, + @ILabelService private readonly labelService: ILabelService, + @IWorkbenchEnvironmentService private readonly workbenchEnvironmentService: IWorkbenchEnvironmentService ) { this.registerViews(); } @@ -180,22 +185,32 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio */ private createExtensionsViewDescriptorsForServer(server: IExtensionManagementServer): IViewDescriptor[] { + const getViewName = (viewTitle: string, server: IExtensionManagementServer): string => { + const serverLabel = this.workbenchEnvironmentService.configuration.remoteAuthority === server.authority ? this.labelService.getHostLabel(REMOTE_HOST_SCHEME, server.authority) || server.label : server.label; + if (viewTitle && this.workbenchEnvironmentService.configuration.remoteAuthority) { + return `${serverLabel} - ${viewTitle}`; + } + return viewTitle ? viewTitle : serverLabel; + }; + const getInstalledViewName = (): string => getViewName(localize('installed', "Installed"), server); + const getOutdatedViewName = (): string => getViewName(localize('outdated', "Outdated"), server); + const onDidChangeServerLabel: EventOf = this.workbenchEnvironmentService.configuration.remoteAuthority ? EventOf.map(this.labelService.onDidChangeFormatters, () => undefined) : EventOf.None; return [{ id: `extensions.${server.authority}.installed`, - name: localize('installed', "Installed"), - ctorDescriptor: { ctor: ServerExtensionsView, arguments: [server] }, + get name() { return getInstalledViewName(); }, + ctorDescriptor: { ctor: ServerExtensionsView, arguments: [server, EventOf.map(onDidChangeServerLabel, () => getInstalledViewName())] }, when: ContextKeyExpr.and(ContextKeyExpr.has('searchInstalledExtensions')), weight: 100 }, { id: `extensions.${server.authority}.outdated`, - name: localize('outdated', "Outdated"), - ctorDescriptor: { ctor: ServerExtensionsView, arguments: [server] }, + get name() { return getOutdatedViewName(); }, + ctorDescriptor: { ctor: ServerExtensionsView, arguments: [server, EventOf.map(onDidChangeServerLabel, () => getOutdatedViewName())] }, when: ContextKeyExpr.and(ContextKeyExpr.has('searchOutdatedExtensions')), weight: 100 }, { id: `extensions.${server.authority}.default`, - name: localize('installed', "Installed"), - ctorDescriptor: { ctor: ServerExtensionsView, arguments: [server] }, + get name() { return getInstalledViewName(); }, + ctorDescriptor: { ctor: ServerExtensionsView, arguments: [server, EventOf.map(onDidChangeServerLabel, () => getInstalledViewName())] }, when: ContextKeyExpr.and(ContextKeyExpr.has('defaultExtensionViews'), ContextKeyExpr.has('hasInstalledExtensions'), RemoteAuthorityContext.notEqualsTo('')), weight: 40, order: 1 @@ -228,7 +243,6 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio ctorDescriptor: { ctor: RecommendedExtensionsView }, when: ContextKeyExpr.has('recommendedExtensions'), weight: 50, - canToggleVisibility: true, order: 2 }; } @@ -243,7 +257,6 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio ctorDescriptor: { ctor: WorkspaceRecommendedExtensionsView }, when: ContextKeyExpr.and(ContextKeyExpr.has('recommendedExtensions'), ContextKeyExpr.has('nonEmptyWorkspace')), weight: 50, - canToggleVisibility: true, order: 1 }; } @@ -256,7 +269,6 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio ctorDescriptor: { ctor: EnabledExtensionsView }, when: ContextKeyExpr.and(ContextKeyExpr.has('searchEnabledExtensions')), weight: 40, - canToggleVisibility: true, order: 1 }; } @@ -269,7 +281,6 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio ctorDescriptor: { ctor: DisabledExtensionsView }, when: ContextKeyExpr.and(ContextKeyExpr.has('searchDisabledExtensions')), weight: 10, - canToggleVisibility: true, order: 3, collapsed: true }; @@ -282,8 +293,7 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio name: viewIdNameMappings[id], ctorDescriptor: { ctor: BuiltInExtensionsView }, when: ContextKeyExpr.has('searchBuiltInExtensions'), - weight: 100, - canToggleVisibility: true + weight: 100 }; } @@ -294,8 +304,7 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio name: viewIdNameMappings[id], ctorDescriptor: { ctor: BuiltInThemesExtensionsView }, when: ContextKeyExpr.has('searchBuiltInExtensions'), - weight: 100, - canToggleVisibility: true + weight: 100 }; } @@ -306,8 +315,7 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio name: viewIdNameMappings[id], ctorDescriptor: { ctor: BuiltInBasicsExtensionsView }, when: ContextKeyExpr.has('searchBuiltInExtensions'), - weight: 100, - canToggleVisibility: true + weight: 100 }; } } diff --git a/src/vs/workbench/contrib/extensions/electron-browser/extensionsViews.ts b/src/vs/workbench/contrib/extensions/electron-browser/extensionsViews.ts index 477bdb8972..a3b4a9113a 100644 --- a/src/vs/workbench/contrib/extensions/electron-browser/extensionsViews.ts +++ b/src/vs/workbench/contrib/extensions/electron-browser/extensionsViews.ts @@ -16,7 +16,7 @@ import { IContextMenuService } from 'vs/platform/contextview/browser/contextView import { append, $, toggleClass } from 'vs/base/browser/dom'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { Delegate, Renderer, IExtensionsViewState } from 'vs/workbench/contrib/extensions/electron-browser/extensionsList'; -import { IExtension, IExtensionsWorkbenchService } from '../common/extensions'; +import { IExtension, IExtensionsWorkbenchService, ExtensionState } from '../common/extensions'; import { Query } from '../common/extensionQuery'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IThemeService } from 'vs/platform/theme/common/themeService'; @@ -45,9 +45,7 @@ import { ExtensionType, ExtensionIdentifier, IExtensionDescription, isLanguagePa import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService'; import product from 'vs/platform/product/node/product'; import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async'; -import { ILabelService } from 'vs/platform/label/common/label'; -import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; -import { REMOTE_HOST_SCHEME } from 'vs/platform/remote/common/remoteHosts'; +import { isUIExtension } from 'vs/workbench/services/extensions/node/extensionsUtil'; class ExtensionsViewState extends Disposable implements IExtensionsViewState { @@ -97,7 +95,7 @@ export class ExtensionsListView extends ViewletPanel { @IWorkspaceContextService protected contextService: IWorkspaceContextService, @IExperimentService private readonly experimentService: IExperimentService, @IWorkbenchThemeService private readonly workbenchThemeService: IWorkbenchThemeService, - @IExtensionManagementServerService private readonly extensionManagementServerService: IExtensionManagementServerService + @IExtensionManagementServerService protected readonly extensionManagementServerService: IExtensionManagementServerService ) { super({ ...(options as IViewletPanelOptions), ariaHeaderLabel: options.title }, keybindingService, contextMenuService, configurationService); this.server = options.server; @@ -340,11 +338,20 @@ export class ExtensionsListView extends ViewletPanel { const isE1Running = running1 && this.extensionManagementServerService.getExtensionManagementServer(running1.extensionLocation) === e1.server; const running2 = runningExtensionsById.get(ExtensionIdentifier.toKey(e2.identifier.id)); const isE2Running = running2 && this.extensionManagementServerService.getExtensionManagementServer(running2.extensionLocation) === e2.server; - if ((isE1Running && isE2Running) || (!isE1Running && !isE2Running)) { + if ((isE1Running && isE2Running)) { return e1.displayName.localeCompare(e2.displayName); } const isE1LanguagePackExtension = e1.local && isLanguagePackExtension(e1.local.manifest); const isE2LanguagePackExtension = e2.local && isLanguagePackExtension(e2.local.manifest); + if (!isE1Running && !isE2Running) { + if (isE1LanguagePackExtension) { + return -1; + } + if (isE2LanguagePackExtension) { + return 1; + } + return e1.displayName.localeCompare(e2.displayName); + } if ((isE1Running && isE2LanguagePackExtension) || (isE2Running && isE1LanguagePackExtension)) { return e1.displayName.localeCompare(e2.displayName); } @@ -865,18 +872,11 @@ export class ExtensionsListView extends ViewletPanel { } } -function getViewTitleForServer(viewTitle: string, server: IExtensionManagementServer, labelService: ILabelService, workbenchEnvironmentService: IWorkbenchEnvironmentService): string { - const serverLabel = workbenchEnvironmentService.configuration.remoteAuthority === server.authority ? labelService.getHostLabel(REMOTE_HOST_SCHEME, server.authority) || server.label : server.label; - if (viewTitle && workbenchEnvironmentService.configuration.remoteAuthority) { - return `${serverLabel} - ${viewTitle}`; - } - return viewTitle ? viewTitle : serverLabel; -} - export class ServerExtensionsView extends ExtensionsListView { constructor( server: IExtensionManagementServer, + onDidChangeTitle: Event, options: ExtensionsListViewOptions, @INotificationService notificationService: INotificationService, @IKeybindingService keybindingService: IKeybindingService, @@ -893,15 +893,11 @@ export class ServerExtensionsView extends ExtensionsListView { @IExperimentService experimentService: IExperimentService, @IWorkbenchThemeService workbenchThemeService: IWorkbenchThemeService, @IExtensionsWorkbenchService extensionsWorkbenchService: IExtensionsWorkbenchService, - @ILabelService labelService: ILabelService, - @IWorkbenchEnvironmentService workbenchEnvironmentService: IWorkbenchEnvironmentService, @IExtensionManagementServerService extensionManagementServerService: IExtensionManagementServerService ) { - const viewTitle = options.title; - options.title = getViewTitleForServer(viewTitle, server, labelService, workbenchEnvironmentService); options.server = server; super(options, notificationService, keybindingService, contextMenuService, instantiationService, themeService, extensionService, extensionsWorkbenchService, editorService, tipsService, modeService, telemetryService, configurationService, contextService, experimentService, workbenchThemeService, extensionManagementServerService); - this.disposables.push(labelService.onDidChangeFormatters(() => this.updateTitle(getViewTitleForServer(viewTitle, server, labelService, workbenchEnvironmentService)))); + this.disposables.push(onDidChangeTitle(title => this.updateTitle(title))); } async show(query: string): Promise> { @@ -1044,6 +1040,12 @@ export class WorkspaceRecommendedExtensionsView extends ExtensionsListView { private getRecommendationsToInstall(): Promise { return this.tipsService.getWorkspaceRecommendations() - .then(recommendations => recommendations.filter(({ extensionId }) => !this.extensionsWorkbenchService.local.some(i => areSameExtensions({ id: extensionId }, i.identifier)))); + .then(recommendations => recommendations.filter(({ extensionId }) => { + const extension = this.extensionsWorkbenchService.local.filter(i => areSameExtensions({ id: extensionId }, i.identifier))[0]; + if (!extension || !extension.local || extension.state !== ExtensionState.Installed) { + return true; + } + return isUIExtension(extension.local.manifest, this.configurationService) ? extension.server !== this.extensionManagementServerService.localExtensionManagementServer : extension.server !== this.extensionManagementServerService.remoteExtensionManagementServer; + })); } } diff --git a/src/vs/workbench/contrib/extensions/electron-browser/extensionsWidgets.ts b/src/vs/workbench/contrib/extensions/electron-browser/extensionsWidgets.ts index e565687756..6228de3bd3 100644 --- a/src/vs/workbench/contrib/extensions/electron-browser/extensionsWidgets.ts +++ b/src/vs/workbench/contrib/extensions/electron-browser/extensionsWidgets.ts @@ -5,7 +5,7 @@ import 'vs/css!./media/extensionsWidgets'; import { Disposable, IDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle'; -import { IExtension, IExtensionsWorkbenchService, IExtensionContainer } from '../common/extensions'; +import { IExtension, IExtensionsWorkbenchService, IExtensionContainer, ExtensionState } from '../common/extensions'; import { append, $, addClass } from 'vs/base/browser/dom'; import * as platform from 'vs/base/common/platform'; import { localize } from 'vs/nls'; @@ -148,34 +148,46 @@ export class TooltipWidget extends ExtensionWidget { constructor( private readonly parent: HTMLElement, - private readonly extensionLabelAction: DisabledLabelAction, + private readonly disabledLabelAction: DisabledLabelAction, private readonly recommendationWidget: RecommendationWidget, - private readonly reloadAction: ReloadAction + private readonly reloadAction: ReloadAction, + @IExtensionManagementServerService private readonly extensionManagementServerService: IExtensionManagementServerService, + @ILabelService private readonly labelService: ILabelService, + @IWorkbenchEnvironmentService private readonly workbenchEnvironmentService: IWorkbenchEnvironmentService ) { super(); this._register(Event.any( - this.extensionLabelAction.onDidChange, + this.disabledLabelAction.onDidChange, this.reloadAction.onDidChange, - this.recommendationWidget.onDidChangeTooltip + this.recommendationWidget.onDidChangeTooltip, + this.labelService.onDidChangeFormatters )(() => this.render())); } render(): void { this.parent.title = ''; this.parent.removeAttribute('aria-label'); + this.parent.title = this.getTooltip(); if (this.extension) { - const title = this.getTitle(); - this.parent.title = title; this.parent.setAttribute('aria-label', localize('extension-arialabel', "{0}. {1} Press enter for extension details.", this.extension.displayName)); } } - private getTitle(): string { + private getTooltip(): string { + if (!this.extension) { + return ''; + } if (this.reloadAction.enabled) { return this.reloadAction.tooltip; } - if (this.extensionLabelAction.enabled) { - return this.extensionLabelAction.label; + if (this.disabledLabelAction.label) { + return this.disabledLabelAction.label; + } + if (this.extension.local && this.extension.state === ExtensionState.Installed) { + if (this.extension.server === this.extensionManagementServerService.remoteExtensionManagementServer) { + return localize('extension enabled on remote', "Extension is enabled on '{0}'", this.labelService.getHostLabel(REMOTE_HOST_SCHEME, this.workbenchEnvironmentService.configuration.remoteAuthority)); + } + return localize('extension enabled locally', "Extension is enabled locally."); } return this.recommendationWidget.tooltip; } @@ -252,6 +264,7 @@ export class RemoteBadgeWidget extends ExtensionWidget { constructor( parent: HTMLElement, + private readonly tooltip: boolean, @IExtensionManagementServerService private readonly extensionManagementServerService: IExtensionManagementServerService, @IInstantiationService private readonly instantiationService: IInstantiationService ) { @@ -276,7 +289,7 @@ export class RemoteBadgeWidget extends ExtensionWidget { return; } if (this.extension.server === this.extensionManagementServerService.remoteExtensionManagementServer) { - this.remoteBadge = this.instantiationService.createInstance(RemoteBadge); + this.remoteBadge = this.instantiationService.createInstance(RemoteBadge, this.tooltip); append(this.element, this.remoteBadge.element); } } @@ -294,6 +307,7 @@ class RemoteBadge extends Disposable { readonly element: HTMLElement; constructor( + private readonly tooltip: boolean, @ILabelService private readonly labelService: ILabelService, @IThemeService private readonly themeService: IThemeService, @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, @@ -320,12 +334,14 @@ class RemoteBadge extends Disposable { this._register(this.themeService.onThemeChange(() => applyBadgeStyle())); this._register(this.workspaceContextService.onDidChangeWorkbenchState(() => applyBadgeStyle())); - const updateTitle = () => { - if (this.element) { - this.element.title = localize('remote extension title', "Extension in {0}", this.labelService.getHostLabel(REMOTE_HOST_SCHEME, this.environmentService.configuration.remoteAuthority)); - } - }; - this._register(this.labelService.onDidChangeFormatters(() => updateTitle())); - updateTitle(); + if (this.tooltip) { + const updateTitle = () => { + if (this.element) { + this.element.title = localize('remote extension title', "Extension in {0}", this.labelService.getHostLabel(REMOTE_HOST_SCHEME, this.environmentService.configuration.remoteAuthority)); + } + }; + this._register(this.labelService.onDidChangeFormatters(() => updateTitle())); + updateTitle(); + } } } \ No newline at end of file diff --git a/src/vs/workbench/contrib/extensions/electron-browser/media/extensionActions.css b/src/vs/workbench/contrib/extensions/electron-browser/media/extensionActions.css index a275945e03..a6fc803419 100644 --- a/src/vs/workbench/contrib/extensions/electron-browser/media/extensionActions.css +++ b/src/vs/workbench/contrib/extensions/electron-browser/media/extensionActions.css @@ -7,6 +7,9 @@ padding: 0 5px; outline-offset: 2px; line-height: initial; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; } .monaco-action-bar .action-item .action-label.clear-extensions { @@ -35,7 +38,7 @@ .monaco-action-bar .action-item.disabled .action-label.extension-action.extension-editor-dropdown-action, .monaco-action-bar .action-item.disabled .action-label.extension-action.reload, .monaco-action-bar .action-item.disabled .action-label.disable-status.hide, -.monaco-action-bar .action-item.disabled .action-label.disable-warning.hide, +.monaco-action-bar .action-item.disabled .action-label.system-disable.hide, .monaco-action-bar .action-item.disabled .action-label.extension-status-label.hide, .monaco-action-bar .action-item.disabled .action-label.malicious-status.not-malicious { display: none; @@ -67,24 +70,33 @@ padding-left: 0; } -.extension-editor > .header > .details > .actions > .monaco-action-bar > .actions-container > .action-item > .action-label.disable-warning, -.extensions-viewlet>.extensions .extension>.details>.footer>.monaco-action-bar .action-item .action-label.disable-warning { - cursor: default; - margin: 0.1em; +.extension-editor > .header > .details > .actions > .monaco-action-bar > .actions-container > .action-item > .action-label.system-disable, +.extensions-viewlet>.extensions .extension>.details>.footer>.monaco-action-bar .action-item .action-label.system-disable { + margin: 0.15em; } -.monaco-action-bar .action-item .action-label.disable-warning.icon { +.monaco-action-bar .action-item .action-label.system-disable.icon { opacity: 1; height: 18px; width: 10px; - background: url('status-warning.svg') center center no-repeat; - margin-top: 0.15em } -.vs-dark .monaco-action-bar .action-item .action-label.disable-warning.icon { +.monaco-action-bar .action-item .action-label.system-disable.warning.icon { + background: url('status-warning.svg') center center no-repeat; +} + +.vs-dark .monaco-action-bar .action-item .action-label.system-disable.warning.icon { background: url('status-warning-inverse.svg') center center no-repeat; } +.monaco-action-bar .action-item .action-label.system-disable.info.icon { + background: url('status-info.svg') center center no-repeat; +} + +.vs-dark .monaco-action-bar .action-item .action-label.system-disable.info.icon { + background: url('status-info-inverse.svg') center center no-repeat; +} + .extension-editor>.header>.details>.actions>.monaco-action-bar .action-item .action-label.extension-status-label, .extension-editor>.header>.details>.actions>.monaco-action-bar .action-item .action-label.disable-status, .extension-editor>.header>.details>.actions>.monaco-action-bar .action-item .action-label.malicious-status { diff --git a/src/vs/workbench/contrib/extensions/electron-browser/media/extensionEditor.css b/src/vs/workbench/contrib/extensions/electron-browser/media/extensionEditor.css index 98667a6765..ba5b43fabf 100644 --- a/src/vs/workbench/contrib/extensions/electron-browser/media/extensionEditor.css +++ b/src/vs/workbench/contrib/extensions/electron-browser/media/extensionEditor.css @@ -41,11 +41,13 @@ line-height: 38px; border-radius: 20px; text-align: center; + display: flex; + align-items: center; + justify-content: center; } .extension-editor > .header > .icon-container .extension-remote-badge .octicon { - font-size: 32px; - vertical-align: middle; + font-size: 28px; } .extension-editor > .header > .details { @@ -145,6 +147,9 @@ padding: 1px 6px; } +.extension-editor > .header > .details > .actions > .monaco-action-bar > .actions-container > .action-item > .extension-action { + max-width: 300px; +} .extension-editor > .header > .details > .subtext-container { display: block; diff --git a/src/vs/workbench/contrib/extensions/electron-browser/media/extensionsViewlet.css b/src/vs/workbench/contrib/extensions/electron-browser/media/extensionsViewlet.css index 9d6e694a15..d21056d85d 100644 --- a/src/vs/workbench/contrib/extensions/electron-browser/media/extensionsViewlet.css +++ b/src/vs/workbench/contrib/extensions/electron-browser/media/extensionsViewlet.css @@ -113,6 +113,10 @@ text-align: center; } +.extensions-viewlet > .extensions .monaco-list-row > .extension > .icon-container .extension-remote-badge > .octicon { + vertical-align: middle +} + .extensions-viewlet > .extensions .monaco-list-row > .extension > .details > .header-container > .header > .extension-remote-badge-container { margin-left: 6px; } @@ -123,11 +127,13 @@ line-height: 14px; border-radius: 20px; text-align: center; + display: flex; + align-items: center; + justify-content: center; } .extensions-viewlet > .extensions .monaco-list-row > .extension > .details > .header-container > .header .extension-remote-badge > .octicon { - font-size: 13px; - vertical-align: middle; + font-size: 12px; } .extensions-viewlet.narrow > .extensions .extension > .icon-container, @@ -224,6 +230,14 @@ flex-wrap: wrap-reverse; } +.extensions-viewlet > .extensions .extension > .details > .footer > .monaco-action-bar > .actions-container .extension-action { + max-width: 150px; +} + +.extensions-viewlet.narrow > .extensions .extension > .details > .footer > .monaco-action-bar > .actions-container .extension-action { + max-width: 100px; +} + .extensions-viewlet > .extensions .extension > .details > .footer > .monaco-action-bar .action-label { margin-top: 0.3em; margin-left: 0.3em; diff --git a/src/vs/workbench/contrib/extensions/electron-browser/media/status-info-inverse.svg b/src/vs/workbench/contrib/extensions/electron-browser/media/status-info-inverse.svg new file mode 100644 index 0000000000..d38c363e0e --- /dev/null +++ b/src/vs/workbench/contrib/extensions/electron-browser/media/status-info-inverse.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/workbench/contrib/extensions/electron-browser/media/status-info.svg b/src/vs/workbench/contrib/extensions/electron-browser/media/status-info.svg new file mode 100644 index 0000000000..6e2e22f67b --- /dev/null +++ b/src/vs/workbench/contrib/extensions/electron-browser/media/status-info.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/workbench/contrib/extensions/electron-browser/runtimeExtensionsEditor.ts b/src/vs/workbench/contrib/extensions/electron-browser/runtimeExtensionsEditor.ts index 081ad1d08b..122c32e31e 100644 --- a/src/vs/workbench/contrib/extensions/electron-browser/runtimeExtensionsEditor.ts +++ b/src/vs/workbench/contrib/extensions/electron-browser/runtimeExtensionsEditor.ts @@ -379,12 +379,12 @@ export class RuntimeExtensionsEditor extends BaseEditor { if (element.description.extensionLocation.scheme !== 'file') { const el = $('span'); - el.innerHTML = renderOcticons(`$(rss) ${element.description.extensionLocation.authority}`); + el.innerHTML = renderOcticons(`$(remote) ${element.description.extensionLocation.authority}`); data.msgContainer.appendChild(el); const hostLabel = this._labelService.getHostLabel(REMOTE_HOST_SCHEME, this._environmentService.configuration.remoteAuthority); if (hostLabel) { - el.innerHTML = renderOcticons(`$(rss) ${hostLabel}`); + el.innerHTML = renderOcticons(`$(remote) ${hostLabel}`); } } diff --git a/src/vs/workbench/contrib/extensions/node/extensionsWorkbenchService.ts b/src/vs/workbench/contrib/extensions/node/extensionsWorkbenchService.ts index aa60ee7c50..644a998578 100644 --- a/src/vs/workbench/contrib/extensions/node/extensionsWorkbenchService.ts +++ b/src/vs/workbench/contrib/extensions/node/extensionsWorkbenchService.ts @@ -36,7 +36,7 @@ import * as resources from 'vs/base/common/resources'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { IFileService } from 'vs/platform/files/common/files'; -import { IExtensionManifest, ExtensionType, ExtensionIdentifierWithVersion, IExtension as IPlatformExtension } from 'vs/platform/extensions/common/extensions'; +import { IExtensionManifest, ExtensionType, ExtensionIdentifierWithVersion, IExtension as IPlatformExtension, isLanguagePackExtension } from 'vs/platform/extensions/common/extensions'; // {{SQL CARBON EDIT}} import { isEngineValid } from 'vs/platform/extensions/node/extensionValidator'; @@ -605,15 +605,19 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension } get local(): IExtension[] { - const result = [...this.localExtensions.local]; - if (!this.remoteExtensions) { - return result; - } - result.push(...this.remoteExtensions.local); + const result = [...this.installed]; const byId = groupByExtension(result, r => r.identifier); return byId.reduce((result, extensions) => { result.push(this.getPrimaryExtension(extensions)); return result; }, []); } + get installed(): IExtension[] { + const result = [...this.localExtensions.local]; + if (this.remoteExtensions) { + result.push(...this.remoteExtensions.local); + } + return result; + } + get outdated(): IExtension[] { const allLocal = [...this.localExtensions.local]; if (this.remoteExtensions) { @@ -855,7 +859,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension } return this.installWithProgress(async () => { - // {{SQL CARBON EDIT}} + // {{SQL CARBON EDIT}} remove extensionservice install from gallery if (extensionPolicy === ExtensionsPolicy.allowMicrosoft) { if (extension.publisherDisplayName === 'Microsoft') { await this.downloadOrBrowse(extension).then(() => this.checkAndEnableDisabledDependencies(gallery.identifier)); @@ -916,7 +920,8 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension return Promise.reject(new Error(nls.localize('incompatible', "Unable to install extension '{0}' with version '{1}' as it is not compatible with Azure Data Studio.", extension.gallery!.identifier.id, version))); } return this.installWithProgress(async () => { - await this.extensionService.installFromGallery(gallery); + const extensionService = extension.server && extension.local && !isLanguagePackExtension(extension.local.manifest) ? extension.server.extensionManagementService : this.extensionService; + await extensionService.installFromGallery(gallery); if (extension.latestVersion !== version) { this.ignoreAutoUpdate(new ExtensionIdentifierWithVersion(gallery.identifier, version)); } diff --git a/src/vs/workbench/contrib/files/browser/editors/binaryFileEditor.ts b/src/vs/workbench/contrib/files/browser/editors/binaryFileEditor.ts index 5fa7f0279e..8a2744ee59 100644 --- a/src/vs/workbench/contrib/files/browser/editors/binaryFileEditor.ts +++ b/src/vs/workbench/contrib/files/browser/editors/binaryFileEditor.ts @@ -15,6 +15,7 @@ import { BINARY_FILE_EDITOR_ID } from 'vs/workbench/contrib/files/common/files'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; /** * An implementation of editor for binary files like images. @@ -29,7 +30,8 @@ export class BinaryFileEditor extends BaseBinaryResourceEditor { @IWindowsService private readonly windowsService: IWindowsService, @IEditorService private readonly editorService: IEditorService, @IStorageService storageService: IStorageService, - @ITextFileService textFileService: ITextFileService + @ITextFileService textFileService: ITextFileService, + @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, ) { super( BinaryFileEditor.ID, @@ -40,7 +42,8 @@ export class BinaryFileEditor extends BaseBinaryResourceEditor { telemetryService, themeService, textFileService, - storageService + environmentService, + storageService, ); } diff --git a/src/vs/workbench/contrib/files/browser/files.contribution.ts b/src/vs/workbench/contrib/files/browser/files.contribution.ts index a001110d46..c768f8fe74 100644 --- a/src/vs/workbench/contrib/files/browser/files.contribution.ts +++ b/src/vs/workbench/contrib/files/browser/files.contribution.ts @@ -38,6 +38,7 @@ import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/la import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { ExplorerService } from 'vs/workbench/contrib/files/common/explorerService'; import { SUPPORTED_ENCODINGS } from 'vs/workbench/services/textfile/common/textfiles'; +import { Schemas } from 'vs/base/common/network'; // Viewlet Action export class OpenExplorerViewletAction extends ShowViewletAction { @@ -59,7 +60,7 @@ class FileUriLabelContribution implements IWorkbenchContribution { constructor(@ILabelService labelService: ILabelService) { labelService.registerFormatter({ - scheme: 'file', + scheme: Schemas.file, formatting: { label: '${authority}${path}', separator: sep, @@ -307,6 +308,7 @@ configurationRegistry.registerConfiguration({ }, 'files.hotExit': { 'type': 'string', + 'scope': ConfigurationScope.APPLICATION, 'enum': [HotExitConfiguration.OFF, HotExitConfiguration.ON_EXIT, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE], 'default': HotExitConfiguration.ON_EXIT, 'markdownEnumDescriptions': [ diff --git a/src/vs/workbench/contrib/files/browser/saveErrorHandler.ts b/src/vs/workbench/contrib/files/browser/saveErrorHandler.ts index aef42b2a92..3deffe16ca 100644 --- a/src/vs/workbench/contrib/files/browser/saveErrorHandler.ts +++ b/src/vs/workbench/contrib/files/browser/saveErrorHandler.ts @@ -136,14 +136,15 @@ export class SaveErrorHandler extends Disposable implements ISaveErrorHandler, I const isReadonly = fileOperationError.fileOperationResult === FileOperationResult.FILE_READ_ONLY; const triedToMakeWriteable = isReadonly && fileOperationError.options && (fileOperationError.options as IWriteTextFileOptions).overwriteReadonly; const isPermissionDenied = fileOperationError.fileOperationResult === FileOperationResult.FILE_PERMISSION_DENIED; + const canHandlePermissionOrReadonlyErrors = resource.scheme === Schemas.file; // https://github.com/Microsoft/vscode/issues/48659 - // Save Elevated (cannot write elevated https://github.com/Microsoft/vscode/issues/48659) - if (resource.scheme === Schemas.file && (isPermissionDenied || triedToMakeWriteable)) { + // Save Elevated + if (canHandlePermissionOrReadonlyErrors && (isPermissionDenied || triedToMakeWriteable)) { actions.primary!.push(this.instantiationService.createInstance(SaveElevatedAction, model, triedToMakeWriteable)); } - // Overwrite (cannot overwrite readonly https://github.com/Microsoft/vscode/issues/48659) - else if (resource.scheme === Schemas.file && isReadonly) { + // Overwrite + else if (canHandlePermissionOrReadonlyErrors && isReadonly) { actions.primary!.push(this.instantiationService.createInstance(OverwriteReadonlyAction, model)); } @@ -158,13 +159,14 @@ export class SaveErrorHandler extends Disposable implements ISaveErrorHandler, I // Discard actions.primary!.push(this.instantiationService.createInstance(ExecuteCommandAction, REVERT_FILE_COMMAND_ID, nls.localize('discard', "Discard"))); - if (isReadonly) { + // 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)); } else { message = nls.localize('readonlySaveError', "Failed to save '{0}': File is read-only. Select 'Overwrite' to attempt to make it writeable.", basename(resource)); } - } else if (isPermissionDenied) { + } 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 { message = nls.localize('genericSaveError', "Failed to save '{0}': {1}", basename(resource), toErrorMessage(error, false)); @@ -246,7 +248,7 @@ class ResolveSaveConflictAction extends Action { return this.editorService.openEditor( { - leftResource: URI.from({ scheme: CONFLICT_RESOLUTION_SCHEME, path: resource.fsPath }), + leftResource: resource.with({ scheme: CONFLICT_RESOLUTION_SCHEME }), rightResource: resource, label: editorLabel, options: { pinned: true } diff --git a/src/vs/workbench/contrib/format/browser/formatActionsNone.ts b/src/vs/workbench/contrib/format/browser/formatActionsNone.ts index 9fc57ae620..0f31b7b564 100644 --- a/src/vs/workbench/contrib/format/browser/formatActionsNone.ts +++ b/src/vs/workbench/contrib/format/browser/formatActionsNone.ts @@ -50,7 +50,7 @@ registerEditorAction(class FormatDocumentMultipleAction extends EditorAction { return commandService.executeCommand('editor.action.formatDocument'); } else { const langName = model.getLanguageIdentifier().language; - const message = nls.localize('no.rovider', "There is no formatter for '{0}'-files installed.", langName); + const message = nls.localize('no.provider', "There is no formatter for '{0}'-files installed.", langName); const choice = { label: nls.localize('install.formatter', "Install Formatter..."), run: () => showExtensionQuery(viewletService, `category:formatters ${langName}`) diff --git a/src/vs/workbench/contrib/preferences/browser/settingsLayout.ts b/src/vs/workbench/contrib/preferences/browser/settingsLayout.ts index 580149e3ef..7fb658fe88 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsLayout.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsLayout.ts @@ -159,6 +159,11 @@ export const tocData: ITOCEntry = { id: 'features/comments', label: localize('comments', "Comments"), settings: ['comments.*'] + }, + { + id: 'features/remote', + label: localize('remote', "Remote"), + settings: ['remote.*'] } ] }, diff --git a/src/vs/workbench/contrib/preferences/electron-browser/preferences.contribution.ts b/src/vs/workbench/contrib/preferences/electron-browser/preferences.contribution.ts index 27a011eda5..8fa236cd41 100644 --- a/src/vs/workbench/contrib/preferences/electron-browser/preferences.contribution.ts +++ b/src/vs/workbench/contrib/preferences/electron-browser/preferences.contribution.ts @@ -418,14 +418,14 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon .then(() => { const remoteAuthority = environmentService.configuration.remoteAuthority; const hostLabel = labelService.getHostLabel(REMOTE_HOST_SCHEME, remoteAuthority) || remoteAuthority; - const label = nls.localize('openRemoteSettings', "Open User Settings ({0})", hostLabel); + const label = nls.localize('openRemoteSettings', "Open Remote Settings ({0})", hostLabel); CommandsRegistry.registerCommand(OpenRemoteSettingsAction.ID, serviceAccessor => { serviceAccessor.get(IInstantiationService).createInstance(OpenRemoteSettingsAction, OpenRemoteSettingsAction.ID, label).run(); }); MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: { id: OpenRemoteSettingsAction.ID, - title: { value: label, original: `Preferences: Open User Settings (${hostLabel})` }, + title: { value: label, original: `Preferences: Open Remote Settings (${hostLabel})` }, category: nls.localize('preferencesCategory', "Preferences") }, when: RemoteAuthorityContext.notEqualsTo('') diff --git a/src/vs/workbench/contrib/preferences/electron-browser/settingsEditor2.ts b/src/vs/workbench/contrib/preferences/electron-browser/settingsEditor2.ts index dd0ac81aa1..0b29e17068 100644 --- a/src/vs/workbench/contrib/preferences/electron-browser/settingsEditor2.ts +++ b/src/vs/workbench/contrib/preferences/electron-browser/settingsEditor2.ts @@ -205,7 +205,7 @@ export class SettingsEditor2 extends BaseEditor { this.updateStyles(); } - setInput(input: SettingsEditor2Input, options: SettingsEditorOptions, token: CancellationToken): Promise { + setInput(input: SettingsEditor2Input, options: SettingsEditorOptions | null, token: CancellationToken): Promise { this.inSettingsEditorContextKey.set(true); return super.setInput(input, options, token) .then(() => new Promise(process.nextTick)) // Force setInput to be async @@ -213,10 +213,10 @@ export class SettingsEditor2 extends BaseEditor { return this.render(token); }) .then(() => { + options = options || SettingsEditorOptions.create({}); + if (!this.viewState.settingsTarget) { - if (!options) { - options = SettingsEditorOptions.create({ target: ConfigurationTarget.USER_LOCAL }); - } else if (!options.target) { + if (!options.target) { options.target = ConfigurationTarget.USER_LOCAL; } } @@ -1065,7 +1065,7 @@ export class SettingsEditor2 extends BaseEditor { this.searchInProgress = null; } - this.viewState.filterToCategory = undefined; + this.tocTree.setFocus([]); this.tocTreeModel.currentSearchModel = this.searchResultModel; this.onSearchModeToggled(); @@ -1206,8 +1206,7 @@ export class SettingsEditor2 extends BaseEditor { this.tocTreeModel.update(); } - this.tocTree.setSelection([]); - this.viewState.filterToCategory = undefined; + this.tocTree.setFocus([]); this.tocTree.expandAll(); this.renderTree(undefined, true); diff --git a/src/vs/workbench/contrib/snippets/browser/configureSnippets.ts b/src/vs/workbench/contrib/snippets/browser/configureSnippets.ts index 38c1d8928a..e7dca4560f 100644 --- a/src/vs/workbench/contrib/snippets/browser/configureSnippets.ts +++ b/src/vs/workbench/contrib/snippets/browser/configureSnippets.ts @@ -26,12 +26,12 @@ const id = 'workbench.action.openSnippets'; namespace ISnippetPick { export function is(thing: object): thing is ISnippetPick { - return thing && typeof (thing).filepath === 'string'; + return thing && URI.isUri((thing).filepath); } } interface ISnippetPick extends IQuickPickItem { - filepath: string; + filepath: URI; hint?: true; } @@ -71,7 +71,7 @@ async function computePicks(snippetService: ISnippetsService, envService: IEnvir existing.push({ label: basename(file.location.fsPath), - filepath: file.location.fsPath, + filepath: file.location, description: names.size === 0 ? nls.localize('global.scope', "(global)") : nls.localize('global.1', "({0})", values(names).join(', ')) @@ -83,7 +83,7 @@ async function computePicks(snippetService: ISnippetsService, envService: IEnvir existing.push({ label: basename(file.location.fsPath), description: `(${modeService.getLanguageName(mode)})`, - filepath: file.location.fsPath + filepath: file.location }); seen.add(mode); } @@ -96,15 +96,15 @@ async function computePicks(snippetService: ISnippetsService, envService: IEnvir future.push({ label: mode, description: `(${label})`, - filepath: join(dir, `${mode}.json`), + filepath: URI.file(join(dir, `${mode}.json`)), hint: true }); } } existing.sort((a, b) => { - let a_ext = extname(a.filepath); - let b_ext = extname(b.filepath); + let a_ext = extname(a.filepath.path); + let b_ext = extname(b.filepath.path); if (a_ext === b_ext) { return a.label.localeCompare(b.label); } else if (a_ext === '.code-snippets') { @@ -165,7 +165,7 @@ async function createSnippetFile(scope: string, defaultPath: URI, windowService: } async function createLanguageSnippetFile(pick: ISnippetPick, fileService: IFileService, textFileService: ITextFileService) { - if (await fileService.exists(URI.file(pick.filepath))) { + if (await fileService.exists(pick.filepath)) { return; } const contents = [ @@ -185,7 +185,7 @@ async function createLanguageSnippetFile(pick: ISnippetPick, fileService: IFileS '\t// }', '}' ].join('\n'); - await textFileService.write(URI.file(pick.filepath), contents); + await textFileService.write(pick.filepath, contents); } CommandsRegistry.registerCommand(id, async (accessor): Promise => { @@ -240,7 +240,7 @@ CommandsRegistry.registerCommand(id, async (accessor): Promise => { if (pick.hint) { await createLanguageSnippetFile(pick, fileService, textFileService); } - return opener.open(URI.file(pick.filepath)); + return opener.open(pick.filepath); } }); diff --git a/src/vs/workbench/contrib/stats/node/workspaceStats.ts b/src/vs/workbench/contrib/stats/node/workspaceStats.ts index 93bf7e7184..9447f01882 100644 --- a/src/vs/workbench/contrib/stats/node/workspaceStats.ts +++ b/src/vs/workbench/contrib/stats/node/workspaceStats.ts @@ -246,8 +246,7 @@ export class WorkspaceStats implements IWorkbenchContribution { /* __GDPR__FRAGMENT__ "WorkspaceTags" : { - "workbench.filesToOpen" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, - "workbench.filesToCreate" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workbench.filesToOpenOrCreate" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workbench.filesToDiff" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.id" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "workspace.roots" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, @@ -352,9 +351,8 @@ export class WorkspaceStats implements IWorkbenchContribution { tags['workspace.id'] = workspaceId; - const { filesToOpen, filesToCreate, filesToDiff } = configuration; - tags['workbench.filesToOpen'] = filesToOpen && filesToOpen.length || 0; - tags['workbench.filesToCreate'] = filesToCreate && filesToCreate.length || 0; + const { filesToOpenOrCreate, filesToDiff } = configuration; + tags['workbench.filesToOpenOrCreate'] = filesToOpenOrCreate && filesToOpenOrCreate.length || 0; tags['workbench.filesToDiff'] = filesToDiff && filesToDiff.length || 0; const isEmpty = state === WorkbenchState.EMPTY; @@ -586,11 +584,9 @@ export class WorkspaceStats implements IWorkbenchContribution { return folder && [folder]; } - private findFolder({ filesToOpen, filesToCreate, filesToDiff }: IWindowConfiguration): URI | undefined { - if (filesToOpen && filesToOpen.length) { - return this.parentURI(filesToOpen[0].fileUri); - } else if (filesToCreate && filesToCreate.length) { - return this.parentURI(filesToCreate[0].fileUri); + private findFolder({ filesToOpenOrCreate, filesToDiff }: IWindowConfiguration): URI | undefined { + if (filesToOpenOrCreate && filesToOpenOrCreate.length) { + return this.parentURI(filesToOpenOrCreate[0].fileUri); } else if (filesToDiff && filesToDiff.length) { return this.parentURI(filesToDiff[0].fileUri); } diff --git a/src/vs/workbench/contrib/telemetry/browser/telemetry.contribution.ts b/src/vs/workbench/contrib/telemetry/browser/telemetry.contribution.ts index a8caad21e1..219d3e0fbb 100644 --- a/src/vs/workbench/contrib/telemetry/browser/telemetry.contribution.ts +++ b/src/vs/workbench/contrib/telemetry/browser/telemetry.contribution.ts @@ -36,7 +36,7 @@ export class TelemetryContribution extends Disposable implements IWorkbenchContr ) { super(); - const { filesToOpen, filesToCreate, filesToDiff } = environmentService.configuration; + const { filesToOpenOrCreate, filesToDiff } = environmentService.configuration; const activeViewlet = viewletService.getActiveViewlet(); /* __GDPR__ @@ -47,8 +47,7 @@ export class TelemetryContribution extends Disposable implements IWorkbenchContr "windowSize.outerHeight": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "windowSize.outerWidth": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "emptyWorkbench": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, - "workbench.filesToOpen": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, - "workbench.filesToCreate": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workbench.filesToOpenOrCreate": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workbench.filesToDiff": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "customKeybindingsCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "theme": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, @@ -64,8 +63,7 @@ export class TelemetryContribution extends Disposable implements IWorkbenchContr userAgent: navigator.userAgent, windowSize: { innerHeight: window.innerHeight, innerWidth: window.innerWidth, outerHeight: window.outerHeight, outerWidth: window.outerWidth }, emptyWorkbench: contextService.getWorkbenchState() === WorkbenchState.EMPTY, - 'workbench.filesToOpen': filesToOpen && filesToOpen.length || 0, - 'workbench.filesToCreate': filesToCreate && filesToCreate.length || 0, + 'workbench.filesToOpenOrCreate': filesToOpenOrCreate && filesToOpenOrCreate.length || 0, 'workbench.filesToDiff': filesToDiff && filesToDiff.length || 0, customKeybindingsCount: keybindingsService.customKeybindingsCount(), theme: themeService.getColorTheme().id, diff --git a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts index 6a152373f8..327f071ab5 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts @@ -339,7 +339,7 @@ export class CreateNewTerminalAction extends Action { // Don't create the instance if the workspace picker was canceled return null; } - return this.terminalService.createTerminal({ cwd: workspace.uri.fsPath }, true); + return this.terminalService.createTerminal({ cwd: workspace.uri }, true); }); } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts b/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts index 412693c60e..61d21effd2 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts @@ -15,7 +15,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; import { Schemas } from 'vs/base/common/network'; -import { REMOTE_HOST_SCHEME, getRemoteAuthority } from 'vs/platform/remote/common/remoteHosts'; +import { getRemoteAuthority } from 'vs/platform/remote/common/remoteHosts'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IProductService } from 'vs/platform/product/common/product'; import { ITerminalInstanceService } from 'vs/workbench/contrib/terminal/browser/terminal'; @@ -131,7 +131,7 @@ export class TerminalProcessManager implements ITerminalProcessManager { }); } - const activeWorkspaceRootUri = this._historyService.getLastActiveWorkspaceRoot(hasRemoteAuthority ? REMOTE_HOST_SCHEME : undefined); + const activeWorkspaceRootUri = this._historyService.getLastActiveWorkspaceRoot(); this._process = this._instantiationService.createInstance(TerminalProcessExtHostProxy, this._terminalId, shellLaunchConfig, activeWorkspaceRootUri, cols, rows, this._configHelper); } else { this._process = this._launchProcess(shellLaunchConfig, cols, rows); diff --git a/src/vs/workbench/contrib/webview/electron-browser/webviewProtocols.ts b/src/vs/workbench/contrib/webview/electron-browser/webviewProtocols.ts index 3e982d3598..716406ab17 100644 --- a/src/vs/workbench/contrib/webview/electron-browser/webviewProtocols.ts +++ b/src/vs/workbench/contrib/webview/electron-browser/webviewProtocols.ts @@ -34,24 +34,26 @@ export function registerFileProtocol( getRoots: () => ReadonlyArray ) { contents.session.protocol.registerBufferProtocol(protocol, (request, callback: any) => { - if (extensionLocation && extensionLocation.scheme === REMOTE_HOST_SCHEME) { - const requestUri = URI.parse(request.url); - const redirectedUri = URI.from({ - scheme: REMOTE_HOST_SCHEME, - authority: extensionLocation.authority, - path: '/vscode-resource', - query: JSON.stringify({ - requestResourcePath: requestUri.path - }) - }); - resolveContent(textFileService, redirectedUri, getMimeType(requestUri), callback); - return; - } - const requestPath = URI.parse(request.url).path; const normalizedPath = URI.file(requestPath); for (const root of getRoots()) { - if (startsWith(normalizedPath.fsPath, root.fsPath + sep)) { + if (!startsWith(normalizedPath.fsPath, root.fsPath + sep)) { + continue; + } + + if (extensionLocation && extensionLocation.scheme === REMOTE_HOST_SCHEME) { + const requestUri = URI.parse(request.url); + const redirectedUri = URI.from({ + scheme: REMOTE_HOST_SCHEME, + authority: extensionLocation.authority, + path: '/vscode-resource', + query: JSON.stringify({ + requestResourcePath: requestUri.path + }) + }); + resolveContent(textFileService, redirectedUri, getMimeType(requestUri), callback); + return; + } else { resolveContent(textFileService, normalizedPath, getMimeType(normalizedPath), callback); return; } diff --git a/src/vs/workbench/electron-browser/main.contribution.ts b/src/vs/workbench/electron-browser/main.contribution.ts index 158e117120..f95b5526e7 100644 --- a/src/vs/workbench/electron-browser/main.contribution.ts +++ b/src/vs/workbench/electron-browser/main.contribution.ts @@ -652,6 +652,7 @@ import { InstallVSIXAction } from 'vs/workbench/contrib/extensions/electron-brow 'type': 'boolean', 'default': true, 'description': nls.localize('autoDetectHighContrast', "If enabled, will automatically change to high contrast theme if Windows is using a high contrast theme, and to dark theme when switching away from a Windows high contrast theme."), + 'scope': ConfigurationScope.APPLICATION, 'included': isWindows }, 'window.doubleClickIconToClose': { @@ -678,6 +679,7 @@ import { InstallVSIXAction } from 'vs/workbench/contrib/extensions/electron-brow 'type': 'boolean', 'default': true, 'description': nls.localize('window.nativeFullScreen', "Controls if native full-screen should be used on macOS. Disable this option to prevent macOS from creating a new space when going full-screen."), + 'scope': ConfigurationScope.APPLICATION, 'included': isMacintosh }, 'window.clickThroughInactive': { diff --git a/src/vs/workbench/electron-browser/main.ts b/src/vs/workbench/electron-browser/main.ts index e12bf43555..db235c946e 100644 --- a/src/vs/workbench/electron-browser/main.ts +++ b/src/vs/workbench/electron-browser/main.ts @@ -91,7 +91,7 @@ class CodeRendererMain extends Disposable { const filesToWait = this.configuration.filesToWait; const filesToWaitPaths = filesToWait && filesToWait.paths; - [filesToWaitPaths, this.configuration.filesToOpen, this.configuration.filesToCreate, this.configuration.filesToDiff].forEach(paths => { + [filesToWaitPaths, this.configuration.filesToOpenOrCreate, this.configuration.filesToDiff].forEach(paths => { if (Array.isArray(paths)) { paths.forEach(path => { if (path.fileUri) { diff --git a/src/vs/workbench/electron-browser/window.ts b/src/vs/workbench/electron-browser/window.ts index ac55545d8e..373f1ddedb 100644 --- a/src/vs/workbench/electron-browser/window.ts +++ b/src/vs/workbench/electron-browser/window.ts @@ -11,10 +11,10 @@ import * as DOM from 'vs/base/browser/dom'; import { Separator } from 'vs/base/browser/ui/actionbar/actionbar'; import { IAction, Action } from 'vs/base/common/actions'; import { IFileService } from 'vs/platform/files/common/files'; -import { toResource, IUntitledResourceInput, SideBySideEditor } from 'vs/workbench/common/editor'; +import { toResource, IUntitledResourceInput, SideBySideEditor, pathsToEditors } from 'vs/workbench/common/editor'; import { IEditorService, IResourceEditor } from 'vs/workbench/services/editor/common/editorService'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { IWindowsService, IWindowService, IWindowSettings, IOpenFileRequest, IWindowsConfiguration, IAddFoldersRequest, IRunActionInWindowRequest, IPathData, IRunKeybindingInWindowRequest } from 'vs/platform/windows/common/windows'; +import { IWindowsService, IWindowService, IWindowSettings, IOpenFileRequest, IWindowsConfiguration, IAddFoldersRequest, IRunActionInWindowRequest, IRunKeybindingInWindowRequest } from 'vs/platform/windows/common/windows'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { ITitleService } from 'vs/workbench/services/title/common/titleService'; import { IWorkbenchThemeService, VS_HC_THEME } from 'vs/workbench/services/themes/common/workbenchThemeService'; @@ -468,20 +468,16 @@ export class ElectronWindow extends Disposable { this.workspaceEditingService.addFolders(foldersToAdd); } - private onOpenFiles(request: IOpenFileRequest): void { + private async onOpenFiles(request: IOpenFileRequest): Promise { const inputs: IResourceEditor[] = []; const diffMode = !!(request.filesToDiff && (request.filesToDiff.length === 2)); - if (!diffMode && request.filesToOpen) { - inputs.push(...this.toInputs(request.filesToOpen, false)); - } - - if (!diffMode && request.filesToCreate) { - inputs.push(...this.toInputs(request.filesToCreate, true)); + if (!diffMode && request.filesToOpenOrCreate) { + inputs.push(...(await pathsToEditors(request.filesToOpenOrCreate, this.fileService))); } if (diffMode && request.filesToDiff) { - inputs.push(...this.toInputs(request.filesToDiff, false)); + inputs.push(...(await pathsToEditors(request.filesToDiff, this.fileService))); } if (inputs.length) { @@ -521,27 +517,6 @@ export class ElectronWindow extends Disposable { }); } - private toInputs(paths: IPathData[], isNew: boolean): IResourceEditor[] { - return paths.map(p => { - const resource = URI.revive(p.fileUri); - let input: IResourceInput | IUntitledResourceInput; - if (isNew) { - input = { filePath: resource!.fsPath, options: { pinned: true } }; - } else { - input = { resource, options: { pinned: true } }; - } - - if (!isNew && typeof p.lineNumber === 'number' && typeof p.columnNumber === 'number') { - input.options!.selection = { - startLineNumber: p.lineNumber, - startColumn: p.columnNumber - }; - } - - return input; - }); - } - dispose(): void { this.touchBarDisposables = dispose(this.touchBarDisposables); diff --git a/src/vs/workbench/services/dialogs/browser/remoteFileDialog.ts b/src/vs/workbench/services/dialogs/browser/remoteFileDialog.ts index 33f2b19c62..a16e424f52 100644 --- a/src/vs/workbench/services/dialogs/browser/remoteFileDialog.ts +++ b/src/vs/workbench/services/dialogs/browser/remoteFileDialog.ts @@ -34,6 +34,7 @@ interface FileQuickPickItem extends IQuickPickItem { enum UpdateResult { Updated, + Updating, NotUpdated, InvalidPath } @@ -51,6 +52,7 @@ export class RemoteFileDialog { private allowFolderSelection: boolean; private remoteAuthority: string | undefined; private requiresTrailing: boolean; + private trailing: string | undefined; private scheme: string = REMOTE_HOST_SCHEME; private contextKey: IContextKey; private userEnteredPathSegment: string; @@ -149,7 +151,6 @@ export class RemoteFileDialog { this.allowFileSelection = !!this.options.canSelectFiles; this.hidden = false; let homedir: URI = this.options.defaultUri ? this.options.defaultUri : this.workspaceContextService.getWorkspace().folders[0].uri; - let trailing: string | undefined; let stat: IFileStat | undefined; let ext: string = resources.extname(homedir); if (this.options.defaultUri) { @@ -160,14 +161,14 @@ export class RemoteFileDialog { } if (!stat || !stat.isDirectory) { homedir = resources.dirname(this.options.defaultUri); - trailing = resources.basename(this.options.defaultUri); + this.trailing = resources.basename(this.options.defaultUri); } // append extension if (isSave && !ext && this.options.filters) { for (let i = 0; i < this.options.filters.length; i++) { if (this.options.filters[i].extensions[0] !== '*') { ext = '.' + this.options.filters[i].extensions[0]; - trailing = trailing ? trailing + ext : ext; + this.trailing = this.trailing ? this.trailing + ext : ext; break; } } @@ -176,6 +177,7 @@ export class RemoteFileDialog { return new Promise(async (resolve) => { this.filePickBox = this.quickInputService.createQuickPick(); + this.filePickBox.busy = true; this.filePickBox.matchOnLabel = false; this.filePickBox.autoFocusOnList = false; this.filePickBox.ignoreFocusOut = true; @@ -221,6 +223,7 @@ export class RemoteFileDialog { this.options.availableFileSystems.shift(); } this.options.defaultUri = undefined; + this.filePickBox.hide(); if (this.requiresTrailing) { return this.fileDialogService.showSaveDialog(this.options).then(result => { doResolve(this, result); @@ -264,7 +267,7 @@ export class RemoteFileDialog { // onDidChangeValue can also be triggered by the auto complete, so if it looks like the auto complete, don't do anything if (this.isChangeFromUser()) { // If the user has just entered more bad path, don't change anything - if (value !== this.constructFullUserPath() && !this.isBadSubpath(value)) { + if (!equalsIgnoreCase(value, this.constructFullUserPath()) && !this.isBadSubpath(value)) { this.filePickBox.validationMessage = undefined; const valueUri = this.remoteUriFrom(this.trimTrailingSlash(this.filePickBox.value)); let updated: UpdateResult = UpdateResult.NotUpdated; @@ -288,12 +291,13 @@ export class RemoteFileDialog { this.filePickBox.show(); this.contextKey.set(true); - await this.updateItems(homedir, trailing); - if (trailing) { - this.filePickBox.valueSelection = [this.filePickBox.value.length - trailing.length, this.filePickBox.value.length - ext.length]; + await this.updateItems(homedir, this.trailing); + if (this.trailing) { + this.filePickBox.valueSelection = [this.filePickBox.value.length - this.trailing.length, this.filePickBox.value.length - ext.length]; } else { this.filePickBox.valueSelection = [this.filePickBox.value.length, this.filePickBox.value.length]; } + this.filePickBox.busy = false; }); } @@ -302,7 +306,7 @@ export class RemoteFileDialog { } private isChangeFromUser(): boolean { - if ((this.filePickBox.value === this.pathAppend(this.currentFolder, this.userEnteredPathSegment + this.autoCompletePathSegment)) + if (equalsIgnoreCase(this.filePickBox.value, this.pathAppend(this.currentFolder, this.userEnteredPathSegment + this.autoCompletePathSegment)) && (this.activeItem === (this.filePickBox.activeItems ? this.filePickBox.activeItems[0] : undefined))) { return false; } @@ -314,6 +318,7 @@ export class RemoteFileDialog { } private async onDidAccept(): Promise { + this.filePickBox.busy = true; let resolveValue: URI | undefined; let navigateValue: URI | undefined; const trimmedPickBoxValue = ((this.filePickBox.value.length > 1) && this.endsWithSlash(this.filePickBox.value)) ? this.filePickBox.value.substr(0, this.filePickBox.value.length - 1) : this.filePickBox.value; @@ -351,21 +356,25 @@ export class RemoteFileDialog { if (resolveValue) { resolveValue = this.addPostfix(resolveValue); if (await this.validate(resolveValue)) { - return Promise.resolve(resolveValue); + this.filePickBox.busy = false; + return resolveValue; } } else if (navigateValue) { - // Try to navigate into the folder - await this.updateItems(navigateValue); + // Try to navigate into the folder. + await this.updateItems(navigateValue, this.trailing); } else { // validation error. Path does not exist. } - return Promise.resolve(undefined); + this.filePickBox.busy = false; + return undefined; } private async tryUpdateItems(value: string, valueUri: URI): Promise { - if (value[value.length - 1] === '~') { - await this.updateItems(this.userHome); + if (this.filePickBox.busy) { this.badPath = undefined; + return UpdateResult.Updating; + } else if (value[value.length - 1] === '~') { + await this.updateItems(this.userHome); return UpdateResult.Updated; } else if (this.endsWithSlash(value) || (!resources.isEqual(this.currentFolder, resources.dirname(valueUri), true) && resources.isEqualOrParent(this.currentFolder, resources.dirname(valueUri), true))) { let stat: IFileStat | undefined; @@ -409,7 +418,7 @@ export class RemoteFileDialog { const inputBasename = resources.basename(this.remoteUriFrom(value)); // Make sure that the folder whose children we are currently viewing matches the path in the input const userPath = this.constructFullUserPath(); - if (userPath === value.substring(0, userPath.length)) { + if (equalsIgnoreCase(userPath, value.substring(0, userPath.length))) { let hasMatch = false; for (let i = 0; i < this.filePickBox.items.length; i++) { const item = this.filePickBox.items[i]; @@ -424,7 +433,7 @@ export class RemoteFileDialog { this.filePickBox.activeItems = []; } } else { - if (inputBasename !== resources.basename(this.currentFolder)) { + if (!equalsIgnoreCase(inputBasename, resources.basename(this.currentFolder))) { this.userEnteredPathSegment = inputBasename; } else { this.userEnteredPathSegment = ''; @@ -442,24 +451,34 @@ export class RemoteFileDialog { } const itemBasename = quickPickItem.label; // Either force the autocomplete, or the old value should be one smaller than the new value and match the new value. - if (!force && (itemBasename.length >= startingBasename.length) && equalsIgnoreCase(itemBasename.substr(0, startingBasename.length), startingBasename)) { + if (itemBasename === '..') { + // Don't match on the up directory item ever. + this.userEnteredPathSegment = startingValue; + this.autoCompletePathSegment = ''; + this.activeItem = quickPickItem; + if (force) { + // clear any selected text + this.insertText(this.userEnteredPathSegment, ''); + } + return false; + } else if (!force && (itemBasename.length >= startingBasename.length) && equalsIgnoreCase(itemBasename.substr(0, startingBasename.length), startingBasename)) { this.userEnteredPathSegment = startingBasename; this.activeItem = quickPickItem; // Changing the active items will trigger the onDidActiveItemsChanged. Clear the autocomplete first, then set it after. this.autoCompletePathSegment = ''; this.filePickBox.activeItems = [quickPickItem]; - this.autoCompletePathSegment = itemBasename.substr(startingBasename.length); + this.autoCompletePathSegment = this.trimTrailingSlash(itemBasename.substr(startingBasename.length)); this.insertText(startingValue + this.autoCompletePathSegment, this.autoCompletePathSegment); this.filePickBox.valueSelection = [startingValue.length, this.filePickBox.value.length]; return true; - } else if (force && (quickPickItem.label !== (this.userEnteredPathSegment + this.autoCompletePathSegment))) { + } else if (force && (!equalsIgnoreCase(quickPickItem.label, (this.userEnteredPathSegment + this.autoCompletePathSegment)))) { this.userEnteredPathSegment = ''; - this.autoCompletePathSegment = itemBasename; + this.autoCompletePathSegment = this.trimTrailingSlash(itemBasename); this.activeItem = quickPickItem; this.filePickBox.valueSelection = [this.pathFromUri(this.currentFolder, true).length, this.filePickBox.value.length]; // use insert text to preserve undo buffer - this.insertText(this.pathAppend(this.currentFolder, itemBasename), itemBasename); - this.filePickBox.valueSelection = [this.filePickBox.value.length - itemBasename.length, this.filePickBox.value.length]; + this.insertText(this.pathAppend(this.currentFolder, this.autoCompletePathSegment), this.autoCompletePathSegment); + this.filePickBox.valueSelection = [this.filePickBox.value.length - this.autoCompletePathSegment.length, this.filePickBox.value.length]; return true; } else { this.userEnteredPathSegment = startingBasename; @@ -506,21 +525,24 @@ export class RemoteFileDialog { return ((path.length > 1) && this.endsWithSlash(path)) ? path.substr(0, path.length - 1) : path; } - private yesNoPrompt(message: string): Promise { + private yesNoPrompt(uri: URI, message: string): Promise { interface YesNoItem extends IQuickPickItem { value: boolean; } const prompt = this.quickInputService.createQuickPick(); - const no = nls.localize('remoteFileDialog.no', 'No'); - prompt.items = [{ label: no, value: false }, { label: nls.localize('remoteFileDialog.yes', 'Yes'), value: true }]; prompt.title = message; - prompt.placeholder = no; + prompt.ignoreFocusOut = true; + prompt.ok = true; + prompt.customButton = true; + prompt.customLabel = nls.localize('remoteFileDialog.cancel', 'Cancel'); + prompt.value = this.pathFromUri(uri); + let isResolving = false; return new Promise(resolve => { prompt.onDidAccept(() => { isResolving = true; prompt.hide(); - resolve(prompt.selectedItems ? prompt.selectedItems[0].value : false); + resolve(true); }); prompt.onDidHide(() => { if (!isResolving) { @@ -531,6 +553,12 @@ export class RemoteFileDialog { this.filePickBox.items = this.filePickBox.items; prompt.dispose(); }); + prompt.onDidChangeValue(() => { + prompt.hide(); + }); + prompt.onDidCustom(() => { + prompt.hide(); + }); prompt.show(); }); } @@ -554,7 +582,7 @@ export class RemoteFileDialog { // Replacing a file. // Show a yes/no prompt const message = nls.localize('remoteFileDialog.validateExisting', '{0} already exists. Are you sure you want to overwrite it?', resources.basename(uri)); - return this.yesNoPrompt(message); + return this.yesNoPrompt(uri, message); } else if (!this.isValidBaseName(resources.basename(uri))) { // Filename not allowed this.filePickBox.validationMessage = nls.localize('remoteFileDialog.validateBadFilename', 'Please enter a valid file name.'); @@ -587,15 +615,24 @@ export class RemoteFileDialog { this.userEnteredPathSegment = trailing ? trailing : ''; this.autoCompletePathSegment = ''; const newValue = trailing ? this.pathFromUri(resources.joinPath(newFolder, trailing)) : this.pathFromUri(newFolder, true); - this.currentFolder = this.remoteUriFrom(this.pathFromUri(newFolder, true)); + const oldFolder = this.currentFolder; + const newFolderPath = this.pathFromUri(newFolder, true); + this.currentFolder = this.remoteUriFrom(newFolderPath); return this.createItems(this.currentFolder).then(items => { this.filePickBox.items = items; if (this.allowFolderSelection) { this.filePickBox.activeItems = []; } if (!equalsIgnoreCase(this.filePickBox.value, newValue)) { - this.filePickBox.valueSelection = [0, this.filePickBox.value.length]; - this.insertText(newValue, newValue); + // the user might have continued typing while we were updating. Only update the input box if it doesn't match the directory. + if (!equalsIgnoreCase(this.filePickBox.value.substring(0, newValue.length), newValue)) { + this.filePickBox.valueSelection = [0, this.filePickBox.value.length]; + this.insertText(newValue, newValue); + } else if (equalsIgnoreCase(this.pathFromUri(resources.dirname(oldFolder), true), newFolderPath)) { + // This is the case where the user went up one dir. We need to make sure that we remove the final dir. + this.filePickBox.valueSelection = [newFolderPath.length, this.filePickBox.value.length]; + this.insertText(newValue, ''); + } } this.filePickBox.valueSelection = [this.filePickBox.value.length, this.filePickBox.value.length]; this.filePickBox.busy = false; diff --git a/src/vs/workbench/services/editor/browser/editorService.ts b/src/vs/workbench/services/editor/browser/editorService.ts index 95766b74af..58162b8e58 100644 --- a/src/vs/workbench/services/editor/browser/editorService.ts +++ b/src/vs/workbench/services/editor/browser/editorService.ts @@ -530,16 +530,11 @@ export class EditorService extends Disposable implements EditorServiceImpl { // Untitled file support const untitledInput = input; - if (!untitledInput.resource || typeof untitledInput.filePath === 'string' || (untitledInput.resource instanceof URI && untitledInput.resource.scheme === Schemas.untitled)) { + if (untitledInput.forceUntitled || !untitledInput.resource || (untitledInput.resource && untitledInput.resource.scheme === Schemas.untitled)) { // {{SQL CARBON EDIT}} let modeId: string = untitledInput.language ? untitledInput.language : getFileMode(this.instantiationService, untitledInput.resource); - return convertEditorInput(this.untitledEditorService.createOrGet( - untitledInput.filePath ? URI.file(untitledInput.filePath) : untitledInput.resource, - modeId, - untitledInput.contents, - untitledInput.encoding - ), undefined, this.instantiationService); + return convertEditorInput(this.untitledEditorService.createOrGet(untitledInput.resource, modeId, untitledInput.contents, untitledInput.encoding), undefined, this.instantiationService); } // Resource Editor Support diff --git a/src/vs/workbench/services/editor/test/browser/editorService.test.ts b/src/vs/workbench/services/editor/test/browser/editorService.test.ts index adee0a55a1..d112350ed7 100644 --- a/src/vs/workbench/services/editor/test/browser/editorService.test.ts +++ b/src/vs/workbench/services/editor/test/browser/editorService.test.ts @@ -8,7 +8,7 @@ import { IEditorModel } from 'vs/platform/editor/common/editor'; import { URI } from 'vs/base/common/uri'; import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; import { EditorInput, EditorOptions, IFileEditorInput, IEditorInput } from 'vs/workbench/common/editor'; -import { workbenchInstantiationService, TestStorageService } from 'vs/workbench/test/workbenchTestServices'; +import { workbenchInstantiationService, TestStorageService, NullFileSystemProvider } from 'vs/workbench/test/workbenchTestServices'; import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput'; import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService'; import { EditorService, DelegatingEditorService } from 'vs/workbench/services/editor/browser/editorService'; @@ -27,6 +27,8 @@ import { EditorServiceImpl } from 'vs/workbench/browser/parts/editor/editor'; import { CancellationToken } from 'vs/base/common/cancellation'; import { timeout } from 'vs/base/common/async'; import { toResource } from 'vs/base/test/common/utils'; +import { IFileService } from 'vs/platform/files/common/files'; +import { Disposable } from 'vs/base/common/lifecycle'; // {{SQL CARBON EDIT}} - Disable editor tests /* @@ -66,8 +68,17 @@ export class TestEditorInput extends EditorInput implements IFileEditorInput { this.gotDisposed = true; } } -*/ -suite('Editor service', () => {/* + +class FileServiceProvider extends Disposable { + constructor(scheme: string, @IFileService fileService: IFileService) { + super(); + + this._register(fileService.registerProvider(scheme, new NullFileSystemProvider())); + } +} + +*/suite('Editor service', () => {/* + function registerTestEditorInput(): void { Registry.as(Extensions.Editors).registerEditor(new EditorDescriptor(TestEditorControl, 'MyTestEditorForEditorService', 'My Test Editor For Next Editor Service'), new SyncDescriptor(TestEditorInput)); } @@ -250,9 +261,23 @@ suite('Editor service', () => {/* assert(input instanceof UntitledEditorInput); // Untyped Input (untitled with file path) - input = service.createInput({ filePath: '/some/path.txt', options: { selection: { startLineNumber: 1, startColumn: 1 } } }); + input = service.createInput({ resource: URI.file('/some/path.txt'), forceUntitled: true, options: { selection: { startLineNumber: 1, startColumn: 1 } } }); assert(input instanceof UntitledEditorInput); assert.ok((input as UntitledEditorInput).hasAssociatedFilePath); + + // Untyped Input (untitled with untitled resource) + input = service.createInput({ resource: URI.parse('untitled://Untitled-1'), forceUntitled: true, options: { selection: { startLineNumber: 1, startColumn: 1 } } }); + assert(input instanceof UntitledEditorInput); + assert.ok(!(input as UntitledEditorInput).hasAssociatedFilePath); + + // Untyped Input (untitled with custom resource) + const provider = instantiationService.createInstance(FileServiceProvider, 'untitled-custom'); + + input = service.createInput({ resource: URI.parse('untitled-custom://some/path'), forceUntitled: true, options: { selection: { startLineNumber: 1, startColumn: 1 } } }); + assert(input instanceof UntitledEditorInput); + assert.ok((input as UntitledEditorInput).hasAssociatedFilePath); + + provider.dispose(); }); test('delegate', function (done) { diff --git a/src/vs/workbench/services/extensions/node/proxyResolver.ts b/src/vs/workbench/services/extensions/node/proxyResolver.ts index cb9e94d193..2e3727a962 100644 --- a/src/vs/workbench/services/extensions/node/proxyResolver.ts +++ b/src/vs/workbench/services/extensions/node/proxyResolver.ts @@ -481,7 +481,7 @@ function readWindowsCaCertificates() { } async function readMacCaCertificates() { - const stdout = (await promisify(cp.execFile)('/usr/bin/security', ['find-certificate', '-a', '-p'], { encoding: 'utf8' })).stdout; + const stdout = (await promisify(cp.execFile)('/usr/bin/security', ['find-certificate', '-a', '-p'], { encoding: 'utf8', maxBuffer: 1024 * 1024 })).stdout; const seen = {}; const certs = stdout.split(/(?=-----BEGIN CERTIFICATE-----)/g) .filter(pem => !!pem.length && !seen[pem] && (seen[pem] = true)); diff --git a/src/vs/workbench/services/files/common/fileService.ts b/src/vs/workbench/services/files/common/fileService.ts index 839705134b..15d3f7ed76 100644 --- a/src/vs/workbench/services/files/common/fileService.ts +++ b/src/vs/workbench/services/files/common/fileService.ts @@ -157,7 +157,7 @@ export class FileService extends Disposable implements IFileService { } // Bubble up any other error as is - throw error; + throw this.ensureError(error); } } @@ -206,7 +206,7 @@ export class FileService extends Disposable implements IFileService { isReadonly: !!(provider.capabilities & FileSystemProviderCapabilities.Readonly), mtime: stat.mtime, size: stat.size, - etag: etag(stat.mtime, stat.size) + etag: etag({ mtime: stat.mtime, size: stat.size }) }; // check to recurse for directories @@ -306,7 +306,7 @@ export class FileService extends Disposable implements IFileService { await this.doWriteUnbuffered(provider, resource, bufferOrReadable); } } catch (error) { - throw new FileOperationError(localize('err.write', "Unable to write file ({0})", error.toString()), toFileOperationResult(error), options); + throw new FileOperationError(localize('err.write', "Unable to write file ({0})", this.ensureError(error).toString()), toFileOperationResult(error), options); } return this.resolve(resource, { resolveMetadata: true }); @@ -337,7 +337,10 @@ export class FileService extends Disposable implements IFileService { // check for size is a weaker check because it can return a false negative if the file has changed // but to the same length. This is a compromise we take to avoid having to produce checksums of // the file content for comparison which would be much slower to compute. - if (options && typeof options.mtime === 'number' && typeof options.etag === 'string' && options.etag !== ETAG_DISABLED && options.mtime < stat.mtime && options.etag !== etag(stat.size, options.mtime)) { + if ( + options && typeof options.mtime === 'number' && typeof options.etag === 'string' && + options.etag !== ETAG_DISABLED && options.mtime < stat.mtime && options.etag !== etag({ mtime: options.mtime /* not using stat.mtime for a reason, see above */, size: stat.size }) + ) { throw new FileOperationError(localize('fileModifiedError', "File Modified Since"), FileOperationResult.FILE_MODIFIED_SINCE, options); } @@ -398,7 +401,7 @@ export class FileService extends Disposable implements IFileService { value: fileStream }; } catch (error) { - throw new FileOperationError(localize('err.read', "Unable to read file ({0})", error.toString()), toFileOperationResult(error), options); + throw new FileOperationError(localize('err.read', "Unable to read file ({0})", this.ensureError(error).toString()), toFileOperationResult(error), options); } } @@ -462,7 +465,7 @@ export class FileService extends Disposable implements IFileService { stream.write(buffer.slice(0, lastChunkLength)); } } catch (error) { - throw error; + throw this.ensureError(error); } finally { await provider.close(handle); } @@ -492,8 +495,8 @@ export class FileService extends Disposable implements IFileService { throw new FileOperationError(localize('fileIsDirectoryError', "Expected file {0} is actually a directory", this.resourceForError(resource)), FileOperationResult.FILE_IS_DIRECTORY, options); } - // Return early if file not modified since - if (options && options.etag === stat.etag) { + // Return early if file not modified since (unless disabled) + if (options && options.etag !== ETAG_DISABLED && options.etag === stat.etag) { throw new FileOperationError(localize('fileNotModifiedError', "File not modified since"), FileOperationResult.FILE_NOT_MODIFIED_SINCE, options); } @@ -863,7 +866,7 @@ export class FileService extends Disposable implements IFileService { posInFile += chunk.byteLength; } } catch (error) { - throw error; + throw this.ensureError(error); } finally { await provider.close(handle); } @@ -929,7 +932,7 @@ export class FileService extends Disposable implements IFileService { } } while (bytesRead > 0); } catch (error) { - throw error; + throw this.ensureError(error); } finally { await Promise.all([ typeof sourceHandle === 'number' ? sourceProvider.close(sourceHandle) : Promise.resolve(), @@ -960,7 +963,7 @@ export class FileService extends Disposable implements IFileService { const buffer = await sourceProvider.readFile(source); await this.doWriteBuffer(targetProvider, targetHandle, VSBuffer.wrap(buffer), buffer.byteLength, 0, 0); } catch (error) { - throw error; + throw this.ensureError(error); } finally { await targetProvider.close(targetHandle); } @@ -991,6 +994,14 @@ export class FileService extends Disposable implements IFileService { return true; } + private ensureError(error?: Error): Error { + if (!error) { + return new Error(localize('unknownError', "Unknown Error")); // https://github.com/Microsoft/vscode/issues/72798 + } + + return error; + } + private throwIfTooLarge(totalBytesRead: number, options?: IReadFileOptions): boolean { // Return early if file is too large to load diff --git a/src/vs/workbench/services/files/test/node/diskFileService.test.ts b/src/vs/workbench/services/files/test/node/diskFileService.test.ts index c63d7c39f8..29d19503eb 100644 --- a/src/vs/workbench/services/files/test/node/diskFileService.test.ts +++ b/src/vs/workbench/services/files/test/node/diskFileService.test.ts @@ -355,7 +355,7 @@ suite('Disk File Service', () => { test('resolve - folder symbolic link', async () => { if (isWindows) { - return; // not happy + return; // not reliable on windows } const link = URI.file(join(testDir, 'deep-link')); @@ -369,7 +369,7 @@ suite('Disk File Service', () => { test('resolve - file symbolic link', async () => { if (isWindows) { - return; // not happy + return; // not reliable on windows } const link = URI.file(join(testDir, 'lorem.txt-linked')); @@ -382,7 +382,7 @@ suite('Disk File Service', () => { test('resolve - invalid symbolic link does not break', async () => { if (isWindows) { - return; // not happy + return; // not reliable on windows } const link = URI.file(join(testDir, 'foo')); @@ -824,12 +824,24 @@ suite('Disk File Service', () => { return testReadFile(URI.file(join(testDir, 'small.txt'))); }); + test('readFile - small file - buffered / readonly', () => { + setCapabilities(fileProvider, FileSystemProviderCapabilities.FileOpenReadWriteClose | FileSystemProviderCapabilities.Readonly); + + return testReadFile(URI.file(join(testDir, 'small.txt'))); + }); + test('readFile - small file - unbuffered', async () => { setCapabilities(fileProvider, FileSystemProviderCapabilities.FileReadWrite); return testReadFile(URI.file(join(testDir, 'small.txt'))); }); + test('readFile - small file - unbuffered / readonly', async () => { + setCapabilities(fileProvider, FileSystemProviderCapabilities.FileReadWrite | FileSystemProviderCapabilities.Readonly); + + return testReadFile(URI.file(join(testDir, 'small.txt'))); + }); + test('readFile - large file - buffered', async () => { setCapabilities(fileProvider, FileSystemProviderCapabilities.FileOpenReadWriteClose); @@ -1200,6 +1212,26 @@ suite('Disk File Service', () => { assert.equal(readFileSync(resource.fsPath), newContent); }); + test('writeFile - buffered - readonly throws', async () => { + setCapabilities(fileProvider, FileSystemProviderCapabilities.FileOpenReadWriteClose | FileSystemProviderCapabilities.Readonly); + + const resource = URI.file(join(testDir, 'small.txt')); + + const content = readFileSync(resource.fsPath); + assert.equal(content, 'Small File'); + + const newContent = 'Updates to the small file'; + + let error: Error; + try { + await service.writeFile(resource, VSBuffer.fromString(newContent)); + } catch (err) { + error = err; + } + + assert.ok(error!); + }); + test('writeFile - unbuffered', async () => { setCapabilities(fileProvider, FileSystemProviderCapabilities.FileReadWrite); @@ -1228,6 +1260,26 @@ suite('Disk File Service', () => { assert.equal(readFileSync(resource.fsPath), newContent); }); + test('writeFile - unbuffered - readonly throws', async () => { + setCapabilities(fileProvider, FileSystemProviderCapabilities.FileReadWrite | FileSystemProviderCapabilities.Readonly); + + const resource = URI.file(join(testDir, 'small.txt')); + + const content = readFileSync(resource.fsPath); + assert.equal(content, 'Small File'); + + const newContent = 'Updates to the small file'; + + let error: Error; + try { + await service.writeFile(resource, VSBuffer.fromString(newContent)); + } catch (err) { + error = err; + } + + assert.ok(error!); + }); + test('writeFile (large file) - multiple parallel writes queue up', async () => { const resource = URI.file(join(testDir, 'lorem.txt')); @@ -1336,20 +1388,25 @@ suite('Disk File Service', () => { assert.equal(readFileSync(resource.fsPath), newContent); }); - test('writeFile (error when writing to file that has been updated meanwhile)', async () => { + test('writeFile - error when writing to file that has been updated meanwhile', async () => { const resource = URI.file(join(testDir, 'small.txt')); const stat = await service.resolve(resource); - const content = readFileSync(resource.fsPath); + const content = readFileSync(resource.fsPath).toString(); assert.equal(content, 'Small File'); const newContent = 'Updates to the small file'; await service.writeFile(resource, VSBuffer.fromString(newContent), { etag: stat.etag, mtime: stat.mtime }); + const newContentLeadingToError = newContent + newContent; + + const fakeMtime = 1000; + const fakeSize = 1000; + let error: FileOperationError | undefined = undefined; try { - await service.writeFile(resource, VSBuffer.fromString(newContent), { etag: etag(0, 0), mtime: 0 }); + await service.writeFile(resource, VSBuffer.fromString(newContentLeadingToError), { etag: etag({ mtime: fakeMtime, size: fakeSize }), mtime: fakeMtime }); } catch (err) { error = err; } @@ -1359,6 +1416,32 @@ suite('Disk File Service', () => { assert.equal(error!.fileOperationResult, FileOperationResult.FILE_MODIFIED_SINCE); }); + test('writeFile - no error when writing to file where size is the same', async () => { + const resource = URI.file(join(testDir, 'small.txt')); + + const stat = await service.resolve(resource); + + const content = readFileSync(resource.fsPath).toString(); + assert.equal(content, 'Small File'); + + const newContent = content; // same content + await service.writeFile(resource, VSBuffer.fromString(newContent), { etag: stat.etag, mtime: stat.mtime }); + + const newContentLeadingToNoError = newContent; // writing the same content should be OK + + const fakeMtime = 1000; + const actualSize = newContent.length; + + let error: FileOperationError | undefined = undefined; + try { + await service.writeFile(resource, VSBuffer.fromString(newContentLeadingToNoError), { etag: etag({ mtime: fakeMtime, size: actualSize }), mtime: fakeMtime }); + } catch (err) { + error = err; + } + + assert.ok(!error); + }); + test('watch - file', done => { const toWatch = URI.file(join(testDir, 'index-watch1.html')); writeFileSync(toWatch.fsPath, 'Init'); @@ -1370,7 +1453,7 @@ suite('Disk File Service', () => { test('watch - file symbolic link', async done => { if (isWindows) { - return done(); // not happy + return done(); // watch tests are flaky on other platforms } const toWatch = URI.file(join(testDir, 'lorem.txt-linked')); @@ -1383,7 +1466,7 @@ suite('Disk File Service', () => { test('watch - file - multiple writes', done => { if (isWindows) { - return done(); // not happy + return done(); // watch tests are flaky on other platforms } const toWatch = URI.file(join(testDir, 'index-watch1.html')); @@ -1446,6 +1529,10 @@ suite('Disk File Service', () => { }); test('watch - folder (non recursive) - change file', done => { + if (!isLinux) { + return done(); // watch tests are flaky on other platforms + } + const watchDir = URI.file(join(testDir, 'watch3')); mkdirSync(watchDir.fsPath); @@ -1458,6 +1545,10 @@ suite('Disk File Service', () => { }); test('watch - folder (non recursive) - add file', done => { + if (!isLinux) { + return done(); // watch tests are flaky on other platforms + } + const watchDir = URI.file(join(testDir, 'watch4')); mkdirSync(watchDir.fsPath); @@ -1469,6 +1560,10 @@ suite('Disk File Service', () => { }); test('watch - folder (non recursive) - delete file', done => { + if (!isLinux) { + return done(); // watch tests are flaky on other platforms + } + const watchDir = URI.file(join(testDir, 'watch5')); mkdirSync(watchDir.fsPath); @@ -1481,6 +1576,10 @@ suite('Disk File Service', () => { }); test('watch - folder (non recursive) - add folder', done => { + if (!isLinux) { + return done(); // watch tests are flaky on other platforms + } + const watchDir = URI.file(join(testDir, 'watch6')); mkdirSync(watchDir.fsPath); @@ -1492,8 +1591,8 @@ suite('Disk File Service', () => { }); test('watch - folder (non recursive) - delete folder', done => { - if (isWindows) { - return done(); // not happy + if (!isLinux) { + return done(); // watch tests are flaky on other platforms } const watchDir = URI.file(join(testDir, 'watch7')); @@ -1508,8 +1607,8 @@ suite('Disk File Service', () => { }); test('watch - folder (non recursive) - symbolic link - change file', async done => { - if (isWindows) { - return done(); // not happy + if (!isLinux) { + return done(); // watch tests are flaky on other platforms } const watchDir = URI.file(join(testDir, 'deep-link')); @@ -1525,7 +1624,7 @@ suite('Disk File Service', () => { test('watch - folder (non recursive) - rename file', done => { if (!isLinux) { - return done(); // not happy + return done(); // watch tests are flaky on other platforms } const watchDir = URI.file(join(testDir, 'watch8')); @@ -1543,7 +1642,7 @@ suite('Disk File Service', () => { test('watch - folder (non recursive) - rename file (different case)', done => { if (!isLinux) { - return done(); // not happy + return done(); // watch tests are flaky on other platforms } const watchDir = URI.file(join(testDir, 'watch8')); diff --git a/src/vs/workbench/services/preferences/browser/preferencesService.ts b/src/vs/workbench/services/preferences/browser/preferencesService.ts index 2eec056a09..ecb2429752 100644 --- a/src/vs/workbench/services/preferences/browser/preferencesService.ts +++ b/src/vs/workbench/services/preferences/browser/preferencesService.ts @@ -137,7 +137,7 @@ export class PreferencesService extends Disposable implements IPreferencesServic } if (this.defaultSettingsRawResource.toString() === uri.toString()) { - const defaultRawSettingsEditorModel = this.instantiationService.createInstance(DefaultRawSettingsEditorModel, this.getDefaultSettings(ConfigurationTarget.USER)); + const defaultRawSettingsEditorModel = this.instantiationService.createInstance(DefaultRawSettingsEditorModel, this.getDefaultSettings(ConfigurationTarget.USER_LOCAL)); const languageSelection = this.modeService.create('jsonc'); const model = this._register(this.modelService.createModel(defaultRawSettingsEditorModel.content, languageSelection, uri)); return Promise.resolve(model); @@ -159,7 +159,7 @@ export class PreferencesService extends Disposable implements IPreferencesServic } if (this.userSettingsResource.toString() === uri.toString()) { - return this.createEditableSettingsEditorModel(ConfigurationTarget.USER, uri); + return this.createEditableSettingsEditorModel(ConfigurationTarget.USER_LOCAL, uri); } const workspaceSettingsUri = this.getEditableSettingsURI(ConfigurationTarget.WORKSPACE); @@ -209,8 +209,8 @@ export class PreferencesService extends Disposable implements IPreferencesServic jsonEditor; return jsonEditor ? - this.openOrSwitchSettings(ConfigurationTarget.USER, this.userSettingsResource, options, group) : - this.openOrSwitchSettings2(ConfigurationTarget.USER, undefined, options, group); + this.openOrSwitchSettings(ConfigurationTarget.USER_LOCAL, this.userSettingsResource, options, group) : + this.openOrSwitchSettings2(ConfigurationTarget.USER_LOCAL, undefined, options, group); } async openRemoteSettings(): Promise { @@ -377,7 +377,7 @@ export class PreferencesService extends Disposable implements IPreferencesServic } public createSettings2EditorModel(): Settings2EditorModel { - return this.instantiationService.createInstance(Settings2EditorModel, this.getDefaultSettings(ConfigurationTarget.USER)); + return this.instantiationService.createInstance(Settings2EditorModel, this.getDefaultSettings(ConfigurationTarget.USER_LOCAL)); } private doOpenSettings2(target: ConfigurationTarget, folderUri: URI | undefined, options?: IEditorOptions, group?: IEditorGroup): Promise { @@ -419,7 +419,7 @@ export class PreferencesService extends Disposable implements IPreferencesServic private getConfigurationTargetFromSettingsResource(resource: URI): ConfigurationTarget { if (this.userSettingsResource.toString() === resource.toString()) { - return ConfigurationTarget.USER; + return ConfigurationTarget.USER_LOCAL; } const workspaceSettingsResource = this.workspaceSettingsResource; @@ -432,11 +432,15 @@ export class PreferencesService extends Disposable implements IPreferencesServic return ConfigurationTarget.WORKSPACE_FOLDER; } - return ConfigurationTarget.USER; + return ConfigurationTarget.USER_LOCAL; } private getConfigurationTargetFromDefaultSettingsResource(uri: URI) { - return this.isDefaultWorkspaceSettingsResource(uri) ? ConfigurationTarget.WORKSPACE : this.isDefaultFolderSettingsResource(uri) ? ConfigurationTarget.WORKSPACE_FOLDER : ConfigurationTarget.USER; + return this.isDefaultWorkspaceSettingsResource(uri) ? + ConfigurationTarget.WORKSPACE : + this.isDefaultFolderSettingsResource(uri) ? + ConfigurationTarget.WORKSPACE_FOLDER : + ConfigurationTarget.USER_LOCAL; } private isDefaultSettingsResource(uri: URI): boolean { diff --git a/src/vs/workbench/services/progress/browser/progressService2.ts b/src/vs/workbench/services/progress/browser/progressService2.ts index 04ecdeee97..1e516d069a 100644 --- a/src/vs/workbench/services/progress/browser/progressService2.ts +++ b/src/vs/workbench/services/progress/browser/progressService2.ts @@ -309,7 +309,7 @@ export class ProgressService2 implements IProgressService2 { disposables.push(attachDialogStyler(dialog, this._themeService)); dialog.show().then(() => { - if (options.cancellable && typeof onDidCancel === 'function') { + if (typeof onDidCancel === 'function') { onDidCancel(); } diff --git a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts index 1d77cd9fc3..42bd2082c0 100644 --- a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts +++ b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts @@ -31,6 +31,7 @@ import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { ILogService } from 'vs/platform/log/common/log'; import { isEqual, isEqualOrParent, extname, basename } from 'vs/base/common/resources'; import { onUnexpectedError } from 'vs/base/common/errors'; +import { Schemas } from 'vs/base/common/network'; /** * The text file editor model listens to changes to its underlying code editor model and saves these changes through the file service back to the disk. @@ -300,7 +301,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil // Decide on etag let etag: string | undefined; if (forceReadFromDisk) { - etag = undefined; // reset ETag if we enforce to read from disk + etag = ETAG_DISABLED; // disable ETag if we enforce to read from disk } else if (this.lastResolvedDiskStat) { etag = this.lastResolvedDiskStat.etag; // otherwise respect etag to support caching } @@ -826,10 +827,11 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil private getTelemetryData(reason: number | undefined): object { const ext = extname(this.resource); const fileName = basename(this.resource); + const path = this.resource.scheme === Schemas.file ? this.resource.fsPath : this.resource.path; const telemetryData = { - mimeType: guessMimeTypes(this.resource.fsPath).join(', '), + mimeType: guessMimeTypes(path).join(', '), ext, - path: hash(this.resource.fsPath), + path: hash(path), reason }; diff --git a/src/vs/workbench/services/textfile/common/textFileService.ts b/src/vs/workbench/services/textfile/common/textFileService.ts index 979e9868cb..445d7d01e6 100644 --- a/src/vs/workbench/services/textfile/common/textFileService.ts +++ b/src/vs/workbench/services/textfile/common/textFileService.ts @@ -871,13 +871,20 @@ export abstract class TextFileService extends Disposable implements ITextFileSer return false; } - // take over encoding and model value from source model + // take over encoding, mode and model value from source model targetModel.updatePreferredEncoding(sourceModel.getEncoding()); if (targetModel.textEditorModel) { const snapshot = sourceModel.createSnapshot(); if (snapshot) { this.modelService.updateModel(targetModel.textEditorModel, createTextBufferFactoryFromSnapshot(snapshot)); } + + if (sourceModel.textEditorModel) { + const language = sourceModel.textEditorModel.getLanguageIdentifier(); + if (language.id > 1) { + targetModel.textEditorModel.setMode(language); // only use if more specific than plain/text + } + } } // save model diff --git a/src/vs/workbench/services/themes/browser/workbenchThemeService.ts b/src/vs/workbench/services/themes/browser/workbenchThemeService.ts index 0e5639dad9..efda47b66a 100644 --- a/src/vs/workbench/services/themes/browser/workbenchThemeService.ts +++ b/src/vs/workbench/services/themes/browser/workbenchThemeService.ts @@ -30,6 +30,7 @@ import { IJSONSchema } from 'vs/base/common/jsonSchema'; import { textmateColorsSchemaId, registerColorThemeSchemas, textmateColorSettingsSchemaId } from 'vs/workbench/services/themes/common/colorThemeSchema'; import { workbenchColorsSchemaId } from 'vs/platform/theme/common/colorRegistry'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { getRemoteAuthority } from 'vs/platform/remote/common/remoteHosts'; // implementation // {{SQL CARBON EDIT}} @@ -480,7 +481,7 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { this.doSetFileIconTheme(newIconTheme); // remember theme data for a quick restore - if (newIconTheme.isLoaded) { + if (newIconTheme.isLoaded && newIconTheme.location && !getRemoteAuthority(newIconTheme.location)) { this.storageService.store(PERSISTED_ICON_THEME_STORAGE_KEY, newIconTheme.toStorageData(), StorageScope.GLOBAL); } diff --git a/src/vs/workbench/test/workbenchTestServices.ts b/src/vs/workbench/test/workbenchTestServices.ts index f9a00ac5be..752b31667a 100644 --- a/src/vs/workbench/test/workbenchTestServices.ts +++ b/src/vs/workbench/test/workbenchTestServices.ts @@ -1017,8 +1017,12 @@ export class TestFileService implements IFileService { onDidChangeFileSystemProviderRegistrations = Event.None; - registerProvider(_scheme: string, _provider: IFileSystemProvider) { - return { dispose() { } }; + private providers = new Map(); + + registerProvider(scheme: string, provider: IFileSystemProvider) { + this.providers.set(scheme, provider); + + return toDisposable(() => this.providers.delete(scheme)); } activateProvider(_scheme: string): Promise { @@ -1026,7 +1030,7 @@ export class TestFileService implements IFileService { } canHandleResource(resource: URI): boolean { - return resource.scheme === 'file'; + return resource.scheme === 'file' || this.providers.has(resource.scheme); } hasCapability(resource: URI, capability: FileSystemProviderCapabilities): boolean { return false; } diff --git a/yarn.lock b/yarn.lock index 9690082eb7..243d3e378d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9257,10 +9257,10 @@ typescript-tslint-plugin@^0.0.7: minimatch "^3.0.4" vscode-languageserver "^5.1.0" -typescript@3.4.1: - version "3.4.1" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.4.1.tgz#b6691be11a881ffa9a05765a205cb7383f3b63c6" - integrity sha512-3NSMb2VzDQm8oBTLH6Nj55VVtUEpe/rgkIzMir0qVoLyjDZlnMBva0U6vDiV3IH+sl/Yu6oP5QwsAQtHPmDd2Q== +typescript@3.4.5: + version "3.4.5" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.4.5.tgz#2d2618d10bb566572b8d7aad5180d84257d70a99" + integrity sha512-YycBxUb49UUhdNMU5aJ7z5Ej2XGmaIBL0x34vZ82fn3hGvD+bgrMrVDpatgz2f7YxUMJxMkbWxJZeAvDxVe7Vw== typescript@^2.6.2: version "2.6.2"