diff --git a/.github/commands.json b/.github/commands.json index de0643d56c9..65936b422a9 100644 --- a/.github/commands.json +++ b/.github/commands.json @@ -133,6 +133,18 @@ "action": "updateLabels", "addLabel": "~needs more info" }, + { + "type": "comment", + "name": "needsPerfInfo", + "allowUsers": [ + "cleidigh", + "usernamehw", + "gjsjohnmurray", + "IllusionMH" + ], + "addLabel": "needs more info", + "comment": "Thanks for creating this issue regarding performance! Please follow this guide to help us diagnose performance issues: https://github.com/microsoft/vscode/wiki/Performance-Issues \n\nHappy Coding!" + }, { "type": "comment", "name": "jsDebugLogs", diff --git a/extensions/github-authentication/src/githubServer.ts b/extensions/github-authentication/src/githubServer.ts index a3124b7c64b..937f47a681a 100644 --- a/extensions/github-authentication/src/githubServer.ts +++ b/extensions/github-authentication/src/githubServer.ts @@ -49,7 +49,7 @@ export class GitHubServer { // TODO@joaomoreno TODO@RMacfarlane private async isNoCorsEnvironment(): Promise { - const uri = await vscode.env.asExternalUri(vscode.Uri.parse(`${vscode.env.uriScheme}://vscode.github-authentication/did-authenticate`)); + const uri = await vscode.env.asExternalUri(vscode.Uri.parse(`${vscode.env.uriScheme}://vscode.github-authentication/dummy`)); return uri.scheme === 'https' && /^vscode\./.test(uri.authority); } diff --git a/extensions/markdown-basics/cgmanifest.json b/extensions/markdown-basics/cgmanifest.json index 0d320ab3376..f3f0717c5ad 100644 --- a/extensions/markdown-basics/cgmanifest.json +++ b/extensions/markdown-basics/cgmanifest.json @@ -33,7 +33,7 @@ "git": { "name": "microsoft/vscode-markdown-tm-grammar", "repositoryUrl": "https://github.com/microsoft/vscode-markdown-tm-grammar", - "commitHash": "399ff6f608a7bef3f68713be23cdcb4c6d475804" + "commitHash": "a612b96d62aa1ce305c4a55dc9d577316fab39da" } }, "license": "MIT", diff --git a/extensions/markdown-basics/syntaxes/markdown.tmLanguage.json b/extensions/markdown-basics/syntaxes/markdown.tmLanguage.json index 66f8114869a..aaa4c774b40 100644 --- a/extensions/markdown-basics/syntaxes/markdown.tmLanguage.json +++ b/extensions/markdown-basics/syntaxes/markdown.tmLanguage.json @@ -4,7 +4,7 @@ "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], - "version": "https://github.com/microsoft/vscode-markdown-tm-grammar/commit/399ff6f608a7bef3f68713be23cdcb4c6d475804", + "version": "https://github.com/microsoft/vscode-markdown-tm-grammar/commit/a612b96d62aa1ce305c4a55dc9d577316fab39da", "name": "Markdown", "scopeName": "text.html.markdown", "patterns": [ @@ -63,7 +63,7 @@ "while": "(^|\\G)\\s*(>) ?" }, "fenced_code_block_css": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(css|css.erb)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(css|css.erb)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -96,7 +96,7 @@ ] }, "fenced_code_block_basic": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(html|htm|shtml|xhtml|inc|tmpl|tpl)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(html|htm|shtml|xhtml|inc|tmpl|tpl)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -129,7 +129,7 @@ ] }, "fenced_code_block_ini": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(ini|conf)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(ini|conf)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -162,7 +162,7 @@ ] }, "fenced_code_block_java": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(java|bsh)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(java|bsh)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -195,7 +195,7 @@ ] }, "fenced_code_block_lua": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(lua)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(lua)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -228,7 +228,7 @@ ] }, "fenced_code_block_makefile": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(Makefile|makefile|GNUmakefile|OCamlMakefile)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(Makefile|makefile|GNUmakefile|OCamlMakefile)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -261,7 +261,7 @@ ] }, "fenced_code_block_perl": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(perl|pl|pm|pod|t|PL|psgi|vcl)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(perl|pl|pm|pod|t|PL|psgi|vcl)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -294,7 +294,7 @@ ] }, "fenced_code_block_r": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(R|r|s|S|Rprofile|\\{\\.r.+?\\})((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(R|r|s|S|Rprofile|\\{\\.r.+?\\})((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -327,7 +327,7 @@ ] }, "fenced_code_block_ruby": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(ruby|rb|rbx|rjs|Rakefile|rake|cgi|fcgi|gemspec|irbrc|Capfile|ru|prawn|Cheffile|Gemfile|Guardfile|Hobofile|Vagrantfile|Appraisals|Rantfile|Berksfile|Berksfile.lock|Thorfile|Puppetfile)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(ruby|rb|rbx|rjs|Rakefile|rake|cgi|fcgi|gemspec|irbrc|Capfile|ru|prawn|Cheffile|Gemfile|Guardfile|Hobofile|Vagrantfile|Appraisals|Rantfile|Berksfile|Berksfile.lock|Thorfile|Puppetfile)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -360,7 +360,7 @@ ] }, "fenced_code_block_php": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(php|php3|php4|php5|phpt|phtml|aw|ctp)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(php|php3|php4|php5|phpt|phtml|aw|ctp)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -396,7 +396,7 @@ ] }, "fenced_code_block_sql": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(sql|ddl|dml)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(sql|ddl|dml)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -429,7 +429,7 @@ ] }, "fenced_code_block_vs_net": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(vb)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(vb)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -462,7 +462,7 @@ ] }, "fenced_code_block_xml": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(xml|xsd|tld|jsp|pt|cpt|dtml|rss|opml)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(xml|xsd|tld|jsp|pt|cpt|dtml|rss|opml)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -495,7 +495,7 @@ ] }, "fenced_code_block_xsl": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(xsl|xslt)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(xsl|xslt)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -528,7 +528,7 @@ ] }, "fenced_code_block_yaml": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(yaml|yml)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(yaml|yml)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -561,7 +561,7 @@ ] }, "fenced_code_block_dosbatch": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(bat|batch)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(bat|batch)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -594,7 +594,7 @@ ] }, "fenced_code_block_clojure": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(clj|cljs|clojure)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(clj|cljs|clojure)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -627,7 +627,7 @@ ] }, "fenced_code_block_coffee": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(coffee|Cakefile|coffee.erb)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(coffee|Cakefile|coffee.erb)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -660,7 +660,7 @@ ] }, "fenced_code_block_c": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(c|h)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(c|h)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -693,7 +693,7 @@ ] }, "fenced_code_block_cpp": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(cpp|c\\+\\+|cxx)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(cpp|c\\+\\+|cxx)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -726,7 +726,7 @@ ] }, "fenced_code_block_diff": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(patch|diff|rej)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(patch|diff|rej)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -759,7 +759,7 @@ ] }, "fenced_code_block_dockerfile": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(dockerfile|Dockerfile)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(dockerfile|Dockerfile)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -792,7 +792,7 @@ ] }, "fenced_code_block_git_commit": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(COMMIT_EDITMSG|MERGE_MSG)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(COMMIT_EDITMSG|MERGE_MSG)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -825,7 +825,7 @@ ] }, "fenced_code_block_git_rebase": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(git-rebase-todo)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(git-rebase-todo)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -858,7 +858,7 @@ ] }, "fenced_code_block_go": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(go|golang)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(go|golang)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -891,7 +891,7 @@ ] }, "fenced_code_block_groovy": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(groovy|gvy)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(groovy|gvy)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -924,7 +924,7 @@ ] }, "fenced_code_block_pug": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(jade|pug)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(jade|pug)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -957,7 +957,7 @@ ] }, "fenced_code_block_js": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(js|jsx|javascript|es6|mjs|cjs|\\{\\.js.+?\\})((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(js|jsx|javascript|es6|mjs|cjs|\\{\\.js.+?\\})((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -990,7 +990,7 @@ ] }, "fenced_code_block_js_regexp": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(regexp)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(regexp)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1023,7 +1023,7 @@ ] }, "fenced_code_block_json": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(json|json5|sublime-settings|sublime-menu|sublime-keymap|sublime-mousemap|sublime-theme|sublime-build|sublime-project|sublime-completions)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(json|json5|sublime-settings|sublime-menu|sublime-keymap|sublime-mousemap|sublime-theme|sublime-build|sublime-project|sublime-completions)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1056,7 +1056,7 @@ ] }, "fenced_code_block_jsonc": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(jsonc)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(jsonc)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1089,7 +1089,7 @@ ] }, "fenced_code_block_less": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(less)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(less)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1122,7 +1122,7 @@ ] }, "fenced_code_block_objc": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(objectivec|objective-c|mm|objc|obj-c|m|h)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(objectivec|objective-c|mm|objc|obj-c|m|h)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1155,7 +1155,7 @@ ] }, "fenced_code_block_swift": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(swift)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(swift)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1188,7 +1188,7 @@ ] }, "fenced_code_block_scss": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(scss)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(scss)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1221,7 +1221,7 @@ ] }, "fenced_code_block_perl6": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(perl6|p6|pl6|pm6|nqp)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(perl6|p6|pl6|pm6|nqp)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1254,7 +1254,7 @@ ] }, "fenced_code_block_powershell": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(powershell|ps1|psm1|psd1)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(powershell|ps1|psm1|psd1)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1287,7 +1287,7 @@ ] }, "fenced_code_block_python": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(python|py|py3|rpy|pyw|cpy|SConstruct|Sconstruct|sconstruct|SConscript|gyp|gypi|\\{\\.python.+?\\})((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(python|py|py3|rpy|pyw|cpy|SConstruct|Sconstruct|sconstruct|SConscript|gyp|gypi|\\{\\.python.+?\\})((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1320,7 +1320,7 @@ ] }, "fenced_code_block_regexp_python": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(re)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(re)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1353,7 +1353,7 @@ ] }, "fenced_code_block_rust": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(rust|rs|\\{\\.rust.+?\\})((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(rust|rs|\\{\\.rust.+?\\})((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1386,7 +1386,7 @@ ] }, "fenced_code_block_scala": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(scala|sbt)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(scala|sbt)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1419,7 +1419,7 @@ ] }, "fenced_code_block_shell": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(shell|sh|bash|zsh|bashrc|bash_profile|bash_login|profile|bash_logout|.textmate_init|\\{\\.bash.+?\\})((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(shell|sh|bash|zsh|bashrc|bash_profile|bash_login|profile|bash_logout|.textmate_init|\\{\\.bash.+?\\})((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1452,7 +1452,7 @@ ] }, "fenced_code_block_ts": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(typescript|ts)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(typescript|ts)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1485,7 +1485,7 @@ ] }, "fenced_code_block_tsx": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(tsx)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(tsx)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1518,7 +1518,7 @@ ] }, "fenced_code_block_csharp": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(cs|csharp|c#)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(cs|csharp|c#)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1551,7 +1551,7 @@ ] }, "fenced_code_block_fsharp": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(fs|fsharp|f#)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(fs|fsharp|f#)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1584,7 +1584,7 @@ ] }, "fenced_code_block_dart": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(dart)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(dart)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1617,7 +1617,7 @@ ] }, "fenced_code_block_handlebars": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(handlebars|hbs)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(handlebars|hbs)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1650,7 +1650,7 @@ ] }, "fenced_code_block_markdown": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(markdown|md)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(markdown|md)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1683,7 +1683,7 @@ ] }, "fenced_code_block_log": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(log)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(log)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1716,7 +1716,7 @@ ] }, "fenced_code_block_erlang": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(erlang)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(erlang)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { @@ -1749,7 +1749,7 @@ ] }, "fenced_code_block_elixir": { - "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(elixir)((\\s+|:|\\{)[^`~]*)?$)", + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(elixir)((\\s+|:|\\{|\\?)[^`~]*)?$)", "name": "markup.fenced_code.block.markdown", "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", "beginCaptures": { diff --git a/extensions/markdown-language-features/notebook/index.ts b/extensions/markdown-language-features/notebook/index.ts index b7c2df85817..7b44fba6583 100644 --- a/extensions/markdown-language-features/notebook/index.ts +++ b/extensions/markdown-language-features/notebook/index.ts @@ -5,35 +5,23 @@ const MarkdownIt = require('markdown-it'); -export async function activate(ctx: { - dependencies: ReadonlyArray<{ entrypoint: string }> -}) { +export function activate() { let markdownIt = new MarkdownIt({ html: true }); - // Should we load the deps before this point? - // Also could we await inside `renderMarkup`? - await Promise.all(ctx.dependencies.map(async (dep) => { - try { - const api = await import(dep.entrypoint); - if (api?.extendMarkdownIt) { - markdownIt = api.extendMarkdownIt(markdownIt); - } - } catch (e) { - console.error('Could not load markdown entryPoint', e); - } - })); - return { - renderMarkup: (context: { element: HTMLElement, content: string }) => { - const rendered = markdownIt.render(context.content); + renderCell: (_id: string, context: { element: HTMLElement, value: string }) => { + const rendered = markdownIt.render(context.value); context.element.innerHTML = rendered; // Insert styles into markdown preview shadow dom so that they are applied for (const markdownStyleNode of document.getElementsByClassName('markdown-style')) { context.element.insertAdjacentElement('beforebegin', markdownStyleNode.cloneNode(true) as Element); } + }, + extendMarkdownIt: (f: (md: typeof markdownIt) => void) => { + f(markdownIt); } }; } diff --git a/extensions/notebook-markdown-extensions/notebook/emoji.ts b/extensions/notebook-markdown-extensions/notebook/emoji.ts index bf82f98ba0f..b842750a03c 100644 --- a/extensions/notebook-markdown-extensions/notebook/emoji.ts +++ b/extensions/notebook-markdown-extensions/notebook/emoji.ts @@ -6,6 +6,12 @@ import type * as markdownIt from 'markdown-it'; const emoji = require('markdown-it-emoji'); -export function extendMarkdownIt(md: markdownIt.MarkdownIt) { - return md.use(emoji); +export function activate(ctx: { + getRenderer: (id: string) => any +}) { + const markdownItRenderer = ctx.getRenderer('markdownItRenderer'); + + markdownItRenderer.extendMarkdownIt((md: markdownIt.MarkdownIt) => { + return md.use(emoji); + }); } diff --git a/extensions/notebook-markdown-extensions/notebook/katex.ts b/extensions/notebook-markdown-extensions/notebook/katex.ts index 910036babf2..ccb12569053 100644 --- a/extensions/notebook-markdown-extensions/notebook/katex.ts +++ b/extensions/notebook-markdown-extensions/notebook/katex.ts @@ -6,23 +6,28 @@ import type * as markdownIt from 'markdown-it'; const styleHref = import.meta.url.replace(/katex.js$/, 'katex.min.css'); -const link = document.createElement('link'); -link.rel = 'stylesheet'; -link.classList.add('markdown-style'); -link.href = styleHref; -document.head.append(link); +export function activate(ctx: { + getRenderer: (id: string) => any +}) { + const markdownItRenderer = ctx.getRenderer('markdownItRenderer'); -const style = document.createElement('style'); -style.classList.add('markdown-style'); -style.textContent = ` - .katex-error { - color: var(--vscode-editorError-foreground); - } -`; -document.head.append(style); + const link = document.createElement('link'); + link.rel = 'stylesheet'; + link.classList.add('markdown-style'); + link.href = styleHref; + document.head.append(link); -const katex = require('@iktakahiro/markdown-it-katex'); + const style = document.createElement('style'); + style.classList.add('markdown-style'); + style.textContent = ` + .katex-error { + color: var(--vscode-editorError-foreground); + } + `; + document.head.append(style); -export function extendMarkdownIt(md: markdownIt.MarkdownIt) { - return md.use(katex); + const katex = require('@iktakahiro/markdown-it-katex'); + markdownItRenderer.extendMarkdownIt((md: markdownIt.MarkdownIt) => { + return md.use(katex); + }); } diff --git a/extensions/notebook-markdown-extensions/package.json b/extensions/notebook-markdown-extensions/package.json index a68a8b07114..ef3911f2eb9 100644 --- a/extensions/notebook-markdown-extensions/package.json +++ b/extensions/notebook-markdown-extensions/package.json @@ -25,24 +25,18 @@ { "id": "markdownItRenderer-katex", "displayName": "Markdown it katex renderer", - "entrypoint": "./notebook-out/katex.js", - "mimeTypes": [ - "text/markdown" - ], - "dependencies": [ - "markdownItRenderer" - ] + "entrypoint": { + "extends": "markdownItRenderer", + "path": "./notebook-out/katex.js" + } }, { "id": "markdownItRenderer-emoji", "displayName": "Markdown it emoji renderer", - "entrypoint": "./notebook-out/emoji.js", - "mimeTypes": [ - "text/markdown" - ], - "dependencies": [ - "markdownItRenderer" - ] + "entrypoint": { + "extends": "markdownItRenderer", + "path": "./notebook-out/emoji.js" + } } ] }, diff --git a/extensions/package.json b/extensions/package.json index f590e5cab37..9b5fd61b741 100644 --- a/extensions/package.json +++ b/extensions/package.json @@ -4,7 +4,7 @@ "license": "MIT", "description": "Dependencies shared by all extensions", "dependencies": { - "typescript": "^4.3.0-dev.20210507" + "typescript": "^4.3.1-rc" }, "scripts": { "postinstall": "node ./postinstall" diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/notebook.document.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/notebook.document.test.ts index cf4e029e497..568295ccddf 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/notebook.document.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/notebook.document.test.ts @@ -47,10 +47,6 @@ suite('Notebook Document', function () { await utils.closeAllEditors(); utils.disposeAll(disposables); disposables.length = 0; - - for (let doc of vscode.notebook.notebookDocuments) { - assert.strictEqual(doc.isDirty, false, doc.uri.toString()); - } }); suiteSetup(function () { @@ -140,6 +136,30 @@ suite('Notebook Document', function () { await p; }); + test('open untitled notebook', async function () { + const nb = await vscode.notebook.openNotebookDocument('notebook.nbdserializer'); + assert.strictEqual(nb.isUntitled, true); + assert.strictEqual(nb.isClosed, false); + assert.strictEqual(nb.uri.scheme, 'untitled'); + // assert.strictEqual(nb.cellCount, 0); // NotebookSerializer ALWAYS returns something here + }); + + test('open untitled with data', async function () { + const nb = await vscode.notebook.openNotebookDocument( + 'notebook.nbdserializer', + new vscode.NotebookData([ + new vscode.NotebookCellData(vscode.NotebookCellKind.Code, 'console.log()', 'javascript'), + new vscode.NotebookCellData(vscode.NotebookCellKind.Markup, 'Hey', 'markdown'), + ]) + ); + assert.strictEqual(nb.isUntitled, true); + assert.strictEqual(nb.isClosed, false); + assert.strictEqual(nb.uri.scheme, 'untitled'); + assert.strictEqual(nb.cellCount, 2); + assert.strictEqual(nb.cellAt(0).kind, vscode.NotebookCellKind.Code); + assert.strictEqual(nb.cellAt(1).kind, vscode.NotebookCellKind.Markup); + }); + test('workspace edit API (replaceCells)', async function () { const uri = await utils.createRandomFile(undefined, undefined, '.nbdtest'); diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/notebook.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/notebook.test.ts index ca3456f934f..b00302b2808 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/notebook.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/notebook.test.ts @@ -104,8 +104,8 @@ suite('Notebook API tests', function () { suiteSetup(function () { suiteDisposables.push(vscode.notebook.registerNotebookContentProvider('notebookCoreTest', { - openNotebook: async (_resource: vscode.Uri): Promise => { - if (/.*empty\-.*\.vsctestnb$/.test(_resource.path)) { + openNotebook: async (resource: vscode.Uri): Promise => { + if (/.*empty\-.*\.vsctestnb$/.test(resource.path)) { return { metadata: new vscode.NotebookDocumentMetadata(), cells: [] diff --git a/extensions/yarn.lock b/extensions/yarn.lock index 18e5f36add7..3d37fa10930 100644 --- a/extensions/yarn.lock +++ b/extensions/yarn.lock @@ -24,10 +24,10 @@ fast-plist@0.1.2: resolved "https://registry.yarnpkg.com/fast-plist/-/fast-plist-0.1.2.tgz#a45aff345196006d406ca6cdcd05f69051ef35b8" integrity sha1-pFr/NFGWAG1AbKbNzQX2kFHvNbg= -typescript@^4.3.0-dev.20210507: - version "4.3.0-dev.20210507" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.3.0-dev.20210507.tgz#07fdc0479bb1b215865aabb01ed1d920cf844ea0" - integrity sha512-SEZV+XOg8exwPXlTmxPT94v9kasblelh4TjL1I12FBv0DiorBHDtUs8GC2h2sg8zJOgFwj06QXiaLLGL5RhzDw== +typescript@^4.3.1-rc: + version "4.3.1-rc" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.3.1-rc.tgz#925149c8d8514e20a6bd8d4bd7f42adac67ab59c" + integrity sha512-L3uJ0gcntaRaKni9aV2amYB+pCDVodKe/B5+IREyvtKGsDOF7cYjchHb/B894skqkgD52ykRuWatIZMqEsHIqA== vscode-grammar-updater@^1.0.3: version "1.0.3" diff --git a/remote/yarn.lock b/remote/yarn.lock index 69069f99d84..efeb6c53ebc 100644 --- a/remote/yarn.lock +++ b/remote/yarn.lock @@ -437,9 +437,9 @@ socks@^2.3.3: smart-buffer "^4.1.0" spdlog@^0.13.0: - version "0.13.4" - resolved "https://registry.yarnpkg.com/spdlog/-/spdlog-0.13.4.tgz#7393d436f077fca1d07500741e50cbf8928a838a" - integrity sha512-tdzk9ysc640emskx+pE/A2JdJ5IAr440ZIsNjRlD9aPK6U6IQ94VUGpl7u0NHamAB8O1H7RxLgtHyXT32V+RaA== + version "0.13.5" + resolved "https://registry.yarnpkg.com/spdlog/-/spdlog-0.13.5.tgz#a31027dcccbe032e9a53579f42cb45428af08bad" + integrity sha512-D1xA5tRXw7eZOoFBCAnOxCxLN3JpHVDjpPJG/xjJ0nFZvtfOUTAzK66MVxJCDht/ZFwjLcBAltvzjfz4JTuSEw== dependencies: bindings "^1.5.0" mkdirp "^0.5.5" diff --git a/src/vs/code/electron-sandbox/issue/issueReporterMain.ts b/src/vs/code/electron-sandbox/issue/issueReporterMain.ts index 5d28a61c896..6e93aa1e4b8 100644 --- a/src/vs/code/electron-sandbox/issue/issueReporterMain.ts +++ b/src/vs/code/electron-sandbox/issue/issueReporterMain.ts @@ -11,6 +11,7 @@ import { ipcRenderer } from 'vs/base/parts/sandbox/electron-sandbox/globals'; import { applyZoom, zoomIn, zoomOut } from 'vs/platform/windows/electron-sandbox/window'; import { $, reset, safeInnerHtml, windowOpenNoOpener } from 'vs/base/browser/dom'; import { Button } from 'vs/base/browser/ui/button/button'; +import { Delayer } from 'vs/base/common/async'; import { groupBy } from 'vs/base/common/collections'; import { debounce } from 'vs/base/common/decorators'; import { Disposable } from 'vs/base/common/lifecycle'; @@ -62,6 +63,7 @@ export class IssueReporter extends Disposable { private receivedPerformanceInfo = false; private shouldQueueSearch = false; private hasBeenSubmitted = false; + private delayedSubmit = new Delayer(300); private readonly previewButton!: Button; @@ -356,7 +358,11 @@ export class IssueReporter extends Disposable { this.searchIssues(title, fileOnExtension, fileOnMarketplace); }); - this.previewButton.onDidClick(() => this.createIssue()); + this.previewButton.onDidClick(async () => { + this.delayedSubmit.trigger(async () => { + this.createIssue(); + }); + }); function sendWorkbenchCommand(commandId: string) { ipcRenderer.send('vscode:workbenchCommand', { id: commandId, from: 'issueReporter' }); @@ -383,9 +389,11 @@ export class IssueReporter extends Disposable { const cmdOrCtrlKey = isMacintosh ? e.metaKey : e.ctrlKey; // Cmd/Ctrl+Enter previews issue and closes window if (cmdOrCtrlKey && e.keyCode === 13) { - if (await this.createIssue()) { - ipcRenderer.send('vscode:closeIssueReporter'); - } + this.delayedSubmit.trigger(async () => { + if (await this.createIssue()) { + ipcRenderer.send('vscode:closeIssueReporter'); + } + }); } // Cmd/Ctrl + w closes issue window diff --git a/src/vs/editor/standalone/browser/quickInput/standaloneQuickInput.css b/src/vs/editor/standalone/browser/quickInput/standaloneQuickInput.css index fc302f49d68..c48737cfaf4 100644 --- a/src/vs/editor/standalone/browser/quickInput/standaloneQuickInput.css +++ b/src/vs/editor/standalone/browser/quickInput/standaloneQuickInput.css @@ -14,7 +14,7 @@ .vs .quick-input-widget .monaco-list-row.focused .monaco-highlighted-label .highlight, .vs .quick-input-widget .monaco-list-row.focused .monaco-highlighted-label .highlight { - color: #33B6FF; + color: #9DDDFF; } .vs-dark .quick-input-widget .monaco-highlighted-label .highlight, diff --git a/src/vs/editor/standalone/common/themes.ts b/src/vs/editor/standalone/common/themes.ts index 24084dacb5e..cf0b7945091 100644 --- a/src/vs/editor/standalone/common/themes.ts +++ b/src/vs/editor/standalone/common/themes.ts @@ -74,7 +74,7 @@ export const vs: IStandaloneThemeData = { [editorIndentGuides]: '#D3D3D3', [editorActiveIndentGuides]: '#939393', [editorSelectionHighlight]: '#ADD6FF4D', - [listFocusHighlightForeground]: '#33B6FF' + [listFocusHighlightForeground]: '#9DDDFF' } }; /* -------------------------------- End vs theme -------------------------------- */ diff --git a/src/vs/platform/log/node/spdlogLog.ts b/src/vs/platform/log/node/spdlogLog.ts index e9fd6a709ee..61c411cc675 100644 --- a/src/vs/platform/log/node/spdlogLog.ts +++ b/src/vs/platform/log/node/spdlogLog.ts @@ -11,6 +11,7 @@ async function createSpdLogLogger(name: string, logfilePath: string, filesize: n // Do not crash if spdlog cannot be loaded try { const _spdlog = await import('spdlog'); + _spdlog.setFlushOn(LogLevel.Info); return _spdlog.createAsyncRotatingLogger(name, logfilePath, filesize, filecount); } catch (e) { console.error(e); @@ -20,6 +21,7 @@ async function createSpdLogLogger(name: string, logfilePath: string, filesize: n export function createRotatingLogger(name: string, filename: string, filesize: number, filecount: number): Promise { const _spdlog: typeof spdlog = require.__$__nodeRequire('spdlog'); + _spdlog.setFlushOn(LogLevel.Info); return _spdlog.createRotatingLogger(name, filename, filesize, filecount); } @@ -38,7 +40,6 @@ function log(logger: spdlog.Logger, level: LogLevel, message: string): void { case LogLevel.Critical: logger.critical(message); break; default: throw new Error('Invalid log level'); } - logger.flush(); } export class SpdLogLogger extends AbstractMessageLogger implements ILogger { diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 73a0d360a47..b2985b4990f 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -1677,6 +1677,17 @@ declare module 'vscode' { */ export function openNotebookDocument(uri: Uri): Thenable; + /** + * Open an untitled notebook. The editor will prompt the user for a file + * path when the document is to be saved. + * + * @see {@link openNotebookDocument} + * @param viewType The notebook view type that should be used. + * @param content The initial contents of the notebook. + * @returns A promise that resolves to a {@link NotebookDocument notebook}. + */ + export function openNotebookDocument(viewType: string, content?: NotebookData): Thenable; + /** * An event that is emitted when a {@link NotebookDocument notebook} is opened. */ diff --git a/src/vs/workbench/api/browser/mainThreadNotebookDocuments.ts b/src/vs/workbench/api/browser/mainThreadNotebookDocuments.ts index 3d2785cd483..a406d945ee0 100644 --- a/src/vs/workbench/api/browser/mainThreadNotebookDocuments.ts +++ b/src/vs/workbench/api/browser/mainThreadNotebookDocuments.ts @@ -9,13 +9,14 @@ import { URI, UriComponents } from 'vs/base/common/uri'; import { BoundModelReferenceCollection } from 'vs/workbench/api/browser/mainThreadDocuments'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; -import { IImmediateCellEditOperation, IMainCellDto, NotebookCellsChangeType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { IImmediateCellEditOperation, IMainCellDto, NotebookCellsChangeType, NotebookDataDto } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { INotebookEditorModelResolverService } from 'vs/workbench/contrib/notebook/common/notebookEditorModelResolverService'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; import { ExtHostContext, ExtHostNotebookShape, IExtHostContext, MainThreadNotebookDocumentsShape } from '../common/extHost.protocol'; import { MainThreadNotebooksAndEditors } from 'vs/workbench/api/browser/mainThreadNotebookDocumentsAndEditors'; import { onUnexpectedError } from 'vs/base/common/errors'; +import { Schemas } from 'vs/base/common/network'; export class MainThreadNotebookDocuments implements MainThreadNotebookDocumentsShape { @@ -47,7 +48,6 @@ export class MainThreadNotebookDocuments implements MainThreadNotebookDocumentsS this._disposables.dispose(); this._modelReferenceCollection.dispose(); dispose(this._documentEventListenersMapping.values()); - } private _handleNotebooksAdded(notebooks: readonly NotebookTextModel[]): void { @@ -119,14 +119,48 @@ export class MainThreadNotebookDocuments implements MainThreadNotebookDocumentsS }; } - async $tryOpenDocument(uriComponents: UriComponents): Promise { + async $tryCreateNotebook(options: { viewType: string, content?: NotebookDataDto }): Promise { + + // find a free URI for the untitled case + let uri: URI; + for (let counter = 1; ; counter++) { + let candidate = URI.from({ scheme: Schemas.untitled, path: `Untitled-${counter}`, query: options.viewType }); + if (!this._notebookService.getNotebookTextModel(candidate)) { + uri = candidate; + break; + } + } + + const ref = await this._notebookEditorModelResolverService.resolve(uri, options.viewType); + + // untitled notebooks are disposed when they get saved. we should not hold a reference + // to such a disposed notebook and therefore dispose the reference as well + ref.object.notebook.onWillDispose(() => { + ref.dispose(); + }); + + // untitled notebooks are dirty by default + this._proxy.$acceptDirtyStateChanged(uri, true); + + // apply content changes... slightly HACKY -> this triggers a change event + if (options.content) { + ref.object.notebook.reset( + options.content.cells, + options.content.metadata, + ref.object.notebook.transientOptions + ); + } + return uri; + } + + async $tryOpenNotebook(uriComponents: UriComponents): Promise { const uri = URI.revive(uriComponents); const ref = await this._notebookEditorModelResolverService.resolve(uri, undefined); this._modelReferenceCollection.add(uri, ref); return uri; } - async $trySaveDocument(uriComponents: UriComponents) { + async $trySaveNotebook(uriComponents: UriComponents) { const uri = URI.revive(uriComponents); const ref = await this._notebookEditorModelResolverService.resolve(uri); diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 5c5f06be704..282ccda9f43 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1033,9 +1033,18 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I // namespace: notebook const notebook: typeof vscode.notebook = { - openNotebookDocument: (uriComponents) => { + async openNotebookDocument(uriOrOptions?: URI | string, content?: vscode.NotebookData) { checkProposedApiEnabled(extension); - return extHostNotebook.openNotebookDocument(uriComponents); + let uri: URI; + if (URI.isUri(uriOrOptions)) { + uri = uriOrOptions; + await extHostNotebook.openNotebookDocument(uriOrOptions); + } else if (typeof uriOrOptions === 'string') { + uri = URI.revive(await extHostNotebook.createNotebookDocument({ viewType: uriOrOptions, content })); + } else { + throw new Error('Invalid arguments'); + } + return extHostNotebook.getNotebookDocument(uri).apiNotebook; }, get onDidOpenNotebookDocument(): Event { checkProposedApiEnabled(extension); diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index aee0a700928..98368960f85 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -888,8 +888,9 @@ export interface MainThreadNotebookEditorsShape extends IDisposable { } export interface MainThreadNotebookDocumentsShape extends IDisposable { - $tryOpenDocument(uriComponents: UriComponents): Promise; - $trySaveDocument(uri: UriComponents): Promise; + $tryCreateNotebook(options: { viewType: string, content?: NotebookDataDto }): Promise; + $tryOpenNotebook(uriComponents: UriComponents): Promise; + $trySaveNotebook(uri: UriComponents): Promise; $applyEdits(resource: UriComponents, edits: IImmediateCellEditOperation[], computeUndoRedo?: boolean): Promise; } diff --git a/src/vs/workbench/api/common/extHostNotebook.ts b/src/vs/workbench/api/common/extHostNotebook.ts index e7956730308..1d2e22cd8ec 100644 --- a/src/vs/workbench/api/common/extHostNotebook.ts +++ b/src/vs/workbench/api/common/extHostNotebook.ts @@ -254,12 +254,20 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { return new NotebookEditorDecorationType(this._notebookEditorsProxy, options).value; } + async createNotebookDocument(options: { viewType: string, content?: vscode.NotebookData }): Promise { + const canonicalUri = await this._notebookDocumentsProxy.$tryCreateNotebook({ + viewType: options.viewType, + content: options.content && typeConverters.NotebookData.from(options.content) + }); + return URI.revive(canonicalUri); + } + async openNotebookDocument(uri: URI): Promise { const cached = this._documents.get(uri); if (cached) { return cached.apiNotebook; } - const canonicalUri = await this._notebookDocumentsProxy.$tryOpenDocument(uri); + const canonicalUri = await this._notebookDocumentsProxy.$tryOpenNotebook(uri); const document = this._documents.get(URI.revive(canonicalUri)); return assertIsDefined(document?.apiNotebook); } @@ -358,19 +366,8 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { if (!serializer) { throw new Error('NO serializer found'); } - const data = await serializer.deserializeNotebook(bytes.buffer, token); - const res: NotebookDataDto = { - metadata: typeConverters.NotebookDocumentMetadata.from(data.metadata), - cells: [], - }; - - for (let cell of data.cells) { - extHostTypes.NotebookCellData.validate(cell); - res.cells.push(typeConverters.NotebookCellData.from(cell)); - } - - return res; + return typeConverters.NotebookData.from(data); } async $notebookToData(handle: number, data: NotebookDataDto, token: CancellationToken): Promise { @@ -378,10 +375,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { if (!serializer) { throw new Error('NO serializer found'); } - const bytes = await serializer.serializeNotebook({ - metadata: typeConverters.NotebookDocumentMetadata.to(data.metadata), - cells: data.cells.map(typeConverters.NotebookCellData.to) - }, token); + const bytes = await serializer.serializeNotebook(typeConverters.NotebookData.to(data), token); return VSBuffer.wrap(bytes); } diff --git a/src/vs/workbench/api/common/extHostNotebookDocument.ts b/src/vs/workbench/api/common/extHostNotebookDocument.ts index 131569c1f74..7fd979ad543 100644 --- a/src/vs/workbench/api/common/extHostNotebookDocument.ts +++ b/src/vs/workbench/api/common/extHostNotebookDocument.ts @@ -258,7 +258,7 @@ export class ExtHostNotebookDocument { if (this._disposed) { return Promise.reject(new Error('Notebook has been closed')); } - return this._proxy.$trySaveDocument(this.uri); + return this._proxy.$trySaveNotebook(this.uri); } private _spliceNotebookCells(splices: NotebookCellsSplice2[], initialization: boolean): void { diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index 72eccc15c02..9aaec755c6b 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -1485,6 +1485,28 @@ export namespace NotebookCellKind { } } +export namespace NotebookData { + + export function from(data: vscode.NotebookData): notebooks.NotebookDataDto { + const res: notebooks.NotebookDataDto = { + metadata: NotebookDocumentMetadata.from(data.metadata), + cells: [], + }; + for (let cell of data.cells) { + types.NotebookCellData.validate(cell); + res.cells.push(NotebookCellData.from(cell)); + } + return res; + } + + export function to(data: notebooks.NotebookDataDto): vscode.NotebookData { + return { + metadata: NotebookDocumentMetadata.to(data.metadata), + cells: data.cells.map(NotebookCellData.to) + }; + } +} + export namespace NotebookCellData { export function from(data: vscode.NotebookCellData): notebooks.ICellDto2 { diff --git a/src/vs/workbench/api/node/extHostOutputService.ts b/src/vs/workbench/api/node/extHostOutputService.ts index 8daae98da4e..78a955a7330 100644 --- a/src/vs/workbench/api/node/extHostOutputService.ts +++ b/src/vs/workbench/api/node/extHostOutputService.ts @@ -32,7 +32,6 @@ class OutputAppender { append(content: string): void { this.appender.critical(content); - this.flush(); } flush(): void { diff --git a/src/vs/workbench/browser/parts/banner/bannerPart.ts b/src/vs/workbench/browser/parts/banner/bannerPart.ts index a894eafc722..0a9e336bb29 100644 --- a/src/vs/workbench/browser/parts/banner/bannerPart.ts +++ b/src/vs/workbench/browser/parts/banner/bannerPart.ts @@ -10,7 +10,7 @@ import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { Codicon, registerCodicon } from 'vs/base/common/codicons'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -import { IStorageService, StorageTarget } from 'vs/platform/storage/common/storage'; +import { IStorageService } from 'vs/platform/storage/common/storage'; import { IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { Part } from 'vs/workbench/browser/part'; import { IWorkbenchLayoutService, Parts } from 'vs/workbench/services/layout/browser/layoutService'; @@ -96,9 +96,9 @@ export class BannerPart extends Part implements IBannerService { constructor( @IThemeService themeService: IThemeService, @IWorkbenchLayoutService layoutService: IWorkbenchLayoutService, + @IStorageService storageService: IStorageService, @IContextKeyService private readonly contextKeyService: IContextKeyService, @IInstantiationService private readonly instantiationService: IInstantiationService, - @IStorageService private readonly storageService: IStorageService, ) { super(Parts.BANNER_PART, { hasTitle: false }, themeService, storageService, layoutService); @@ -131,8 +131,8 @@ export class BannerPart extends Part implements IBannerService { clearNode(this.element); // Remember choice - if (item.scope) { - this.storageService.store(item.id, true, item.scope, StorageTarget.USER); + if (typeof item.onClose === 'function') { + item.onClose(); } this.item = undefined; @@ -211,10 +211,6 @@ export class BannerPart extends Part implements IBannerService { } show(item: IBannerItem): void { - if (item.scope && this.storageService.getBoolean(item.id, item.scope, false)) { - return; - } - if (item.id === this.item?.id) { this.setVisibility(true); return; diff --git a/src/vs/workbench/contrib/notebook/browser/constants.ts b/src/vs/workbench/contrib/notebook/browser/constants.ts deleted file mode 100644 index f38ad088c9f..00000000000 --- a/src/vs/workbench/contrib/notebook/browser/constants.ts +++ /dev/null @@ -1,8 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -// Scrollable Element - -export const SCROLLABLE_ELEMENT_PADDING_TOP = 18; diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts b/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts index f8e5427ebdc..2a79d2bf867 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts @@ -1039,7 +1039,11 @@ MenuRegistry.appendMenuItem(MenuId.NotebookToolbar, { }, order: -5, group: 'navigation/add', - when: NOTEBOOK_EDITOR_EDITABLE.isEqualTo(true) + when: ContextKeyExpr.and( + NOTEBOOK_EDITOR_EDITABLE.isEqualTo(true), + ContextKeyExpr.notEquals('config.notebook.experimental.insertToolbarPosition', 'betweenCells'), + ContextKeyExpr.notEquals('config.notebook.experimental.insertToolbarPosition', 'hidden') + ) }); MenuRegistry.appendMenuItem(MenuId.NotebookCellListTop, { @@ -1105,7 +1109,11 @@ MenuRegistry.appendMenuItem(MenuId.NotebookToolbar, { }, order: -5, group: 'navigation/add', - when: NOTEBOOK_EDITOR_EDITABLE.isEqualTo(true) + when: ContextKeyExpr.and( + NOTEBOOK_EDITOR_EDITABLE.isEqualTo(true), + ContextKeyExpr.notEquals('config.notebook.experimental.insertToolbarPosition', 'betweenCells'), + ContextKeyExpr.notEquals('config.notebook.experimental.insertToolbarPosition', 'hidden') + ) }); MenuRegistry.appendMenuItem(MenuId.NotebookCellListTop, { diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/status/editorStatus.ts b/src/vs/workbench/contrib/notebook/browser/contrib/status/editorStatus.ts index 9538da6465c..4a415850196 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/status/editorStatus.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/status/editorStatus.ts @@ -242,6 +242,12 @@ export class KernelStatus extends Disposable implements IWorkbenchContribution { } const updateStatus = () => { + if (activeEditor.notebookOptions.getLayoutConfiguration().globalToolbar) { + // kernel info rendered in the notebook toolbar already + this._kernelInfoElement.clear(); + return; + } + const notebook = activeEditor.viewModel?.notebookDocument; if (notebook) { this._showKernelStatus(notebook); @@ -254,6 +260,7 @@ export class KernelStatus extends Disposable implements IWorkbenchContribution { this._editorDisposables.add(this._notebookKernelService.onDidChangeNotebookKernelBinding(updateStatus)); this._editorDisposables.add(this._notebookKernelService.onDidChangeNotebookAffinity(updateStatus)); this._editorDisposables.add(activeEditor.onDidChangeModel(updateStatus)); + this._editorDisposables.add(activeEditor.notebookOptions.onDidChangeOptions(updateStatus)); updateStatus(); } diff --git a/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffEditor.ts b/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffEditor.ts index 9db388ec10e..75a9258c0ac 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffEditor.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffEditor.ts @@ -143,10 +143,10 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD setMarkdownCellEditState(cellId: string, editState: CellEditState): void { // throw new Error('Method not implemented.'); } - markdownCellDragStart(cellId: string, position: { clientY: number }): void { + markdownCellDragStart(cellId: string, event: { dragOffsetY: number }): void { // throw new Error('Method not implemented.'); } - markdownCellDrag(cellId: string, position: { clientY: number }): void { + markdownCellDrag(cellId: string, event: { dragOffsetY: number }): void { // throw new Error('Method not implemented.'); } markdownCellDragEnd(cellId: string): void { diff --git a/src/vs/workbench/contrib/notebook/browser/extensionPoint.ts b/src/vs/workbench/contrib/notebook/browser/extensionPoint.ts index de640c7926e..959e70bf586 100644 --- a/src/vs/workbench/contrib/notebook/browser/extensionPoint.ts +++ b/src/vs/workbench/contrib/notebook/browser/extensionPoint.ts @@ -6,7 +6,7 @@ import { IJSONSchema } from 'vs/base/common/jsonSchema'; import * as nls from 'vs/nls'; import { ExtensionsRegistry } from 'vs/workbench/services/extensions/common/extensionsRegistry'; -import { NotebookEditorPriority } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { NotebookEditorPriority, NotebookRendererEntrypoint } from 'vs/workbench/contrib/notebook/common/notebookCommon'; namespace NotebookEditorContribution { export const viewType = 'viewType'; @@ -37,7 +37,7 @@ export interface INotebookRendererContribution { readonly [NotebookRendererContribution.viewType]?: string; readonly [NotebookRendererContribution.displayName]: string; readonly [NotebookRendererContribution.mimeTypes]?: readonly string[]; - readonly [NotebookRendererContribution.entrypoint]: string; + readonly [NotebookRendererContribution.entrypoint]: NotebookRendererEntrypoint; readonly [NotebookRendererContribution.hardDependencies]: readonly string[]; readonly [NotebookRendererContribution.optionalDependencies]: readonly string[]; } @@ -130,8 +130,27 @@ const notebookRendererContribution: IJSONSchema = { } }, [NotebookRendererContribution.entrypoint]: { - type: 'string', description: nls.localize('contributes.notebook.renderer.entrypoint', 'File to load in the webview to render the extension.'), + oneOf: [ + { + type: 'string', + }, + // todo@connor4312 + @mjbvz: uncomment this once it's ready for external adoption + // { + // type: 'object', + // required: ['extends', 'path'], + // properties: { + // extends: { + // type: 'string', + // description: nls.localize('contributes.notebook.renderer.entrypoint.extends', 'Existing renderer that this one extends.'), + // }, + // path: { + // type: 'string', + // description: nls.localize('contributes.notebook.renderer.entrypoint', 'File to load in the webview to render the extension.'), + // }, + // } + // } + ] }, [NotebookRendererContribution.hardDependencies]: { type: 'array', diff --git a/src/vs/workbench/contrib/notebook/browser/media/notebook.css b/src/vs/workbench/contrib/notebook/browser/media/notebook.css index 33bfd31588f..cc070968973 100644 --- a/src/vs/workbench/contrib/notebook/browser/media/notebook.css +++ b/src/vs/workbench/contrib/notebook/browser/media/notebook.css @@ -13,7 +13,7 @@ .monaco-workbench .notebookOverlay .notebook-toolbar-container { width: 100%; - display: flex; + display: none; margin-top: 2px; margin-bottom: 2px; } @@ -347,46 +347,6 @@ display: none; } -/* top and bottom borders on cells */ -.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-top:before, -.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-bottom:before, -.monaco-workbench .notebookOverlay .monaco-list .markdown-cell-row .cell-inner-container:before, -.monaco-workbench .notebookOverlay .monaco-list .markdown-cell-row .cell-inner-container:after { - content: ""; - position: absolute; - width: 100%; - height: 1px; -} - -.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-left:before, -.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-right:before { - content: ""; - position: absolute; - width: 1px; - height: 100%; - z-index: 10; -} - -/* top border */ -.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-top:before { - border-top: 1px solid transparent; -} - -/* left border */ -.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-left:before { - border-left: 1px solid transparent; -} - -/* bottom border */ -.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-bottom:before { - border-bottom: 1px solid transparent; -} - -/* right border */ -.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-right:before { - border-right: 1px solid transparent; -} - .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-top:before { top: 0; } diff --git a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts index 7142116ab99..f0051ae83d7 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts @@ -255,7 +255,7 @@ class CellContentProvider implements ITextModelContentProvider { } if (result) { - const once = result.onWillDispose(() => { + const once = Event.any(result.onWillDispose, ref.object.notebook.onWillDispose)(() => { once.dispose(); ref.dispose(); }); diff --git a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts index d188dea2b16..b4dbbc4020b 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts @@ -170,16 +170,16 @@ export interface ICommonNotebookEditor { triggerScroll(event: IMouseWheelEvent): void; getCellByInfo(cellInfo: ICommonCellInfo): IGenericCellViewModel; getCellById(cellId: string): IGenericCellViewModel | undefined; - toggleNotebookCellSelection(cell: IGenericCellViewModel): void; + toggleNotebookCellSelection(cell: IGenericCellViewModel, selectFromPrevious: boolean): void; focusNotebookCell(cell: IGenericCellViewModel, focus: 'editor' | 'container' | 'output', options?: IFocusNotebookCellOptions): void; focusNextNotebookCell(cell: IGenericCellViewModel, focus: 'editor' | 'container' | 'output'): void; updateOutputHeight(cellInfo: ICommonCellInfo, output: IDisplayOutputViewModel, height: number, isInit: boolean, source?: string): void; scheduleOutputHeightAck(cellInfo: ICommonCellInfo, outputId: string, height: number): void; updateMarkdownCellHeight(cellId: string, height: number, isInit: boolean): void; setMarkdownCellEditState(cellId: string, editState: CellEditState): void; - markdownCellDragStart(cellId: string, position: { clientY: number }): void; - markdownCellDrag(cellId: string, position: { clientY: number }): void; - markdownCellDrop(cellId: string, position: { clientY: number, ctrlKey: boolean, altKey: boolean }): void; + markdownCellDragStart(cellId: string, event: { dragOffsetY: number }): void; + markdownCellDrag(cellId: string, event: { dragOffsetY: number }): void; + markdownCellDrop(cellId: string, event: { dragOffsetY: number, ctrlKey: boolean, altKey: boolean }): void; markdownCellDragEnd(cellId: string): void; } diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorToolbar.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorToolbar.ts index 3fc2773c8bb..0d45a0ab1dd 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorToolbar.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorToolbar.ts @@ -23,6 +23,7 @@ import { INotebookEditor, NOTEBOOK_EDITOR_ID } from 'vs/workbench/contrib/notebo import { NotebooKernelActionViewItem } from 'vs/workbench/contrib/notebook/browser/notebookKernelActionViewItem'; import { ActionViewWithLabel } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellActionView'; import { CellMenus } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellMenus'; +import { ExperimentalGlobalToolbar } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { ITASExperimentService } from 'vs/workbench/services/experiment/common/experimentService'; @@ -97,10 +98,10 @@ export class NotebookEditorToolbar extends Disposable { this._notebookGlobalActionsMenu = this._register(cellMenu.getNotebookToolbar(this.contextKeyService)); this._register(this._notebookGlobalActionsMenu); - this._useGlobalToolbar = this.configurationService.getValue('notebook.experimental.globalToolbar') ?? false; + this._useGlobalToolbar = this.configurationService.getValue(ExperimentalGlobalToolbar) ?? false; this._register(this.configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration('notebook.experimental.globalToolbar')) { - this._useGlobalToolbar = this.configurationService.getValue('notebook.experimental.globalToolbar'); + if (e.affectsConfiguration(ExperimentalGlobalToolbar)) { + this._useGlobalToolbar = this.configurationService.getValue(ExperimentalGlobalToolbar); this._showNotebookActionsinEditorToolbar(); } })); diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts index 2307875a22b..53417e2cb69 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts @@ -70,7 +70,6 @@ import { readFontInfo } from 'vs/editor/browser/config/configuration'; import { INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; import { NotebookEditorContextKeys } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidgetContextKeys'; import { NotebookOptions } from 'vs/workbench/contrib/notebook/common/notebookOptions'; -import { SCROLLABLE_ELEMENT_PADDING_TOP } from 'vs/workbench/contrib/notebook/browser/constants'; import { ViewContext } from 'vs/workbench/contrib/notebook/browser/viewModel/viewContext'; import { NotebookEditorToolbar } from 'vs/workbench/contrib/notebook/browser/notebookEditorToolbar'; @@ -213,7 +212,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor private _dndController: CellDragAndDropController | null = null; private _listTopCellToolbar: ListTopCellToolbar | null = null; private _renderedEditors: Map = new Map(); - private _viewContext: ViewContext | undefined; + private _viewContext: ViewContext; private _notebookViewModel: NotebookViewModel | undefined; private _localStore: DisposableStore = this._register(new DisposableStore()); private _localCellStateListeners: DisposableStore[] = []; @@ -334,6 +333,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor this.useRenderer = !isWeb && !!this.configurationService.getValue(ExperimentalUseMarkdownRenderer) && !accessibilityService.isScreenReaderOptimized(); this._notebookOptions = new NotebookOptions(this.configurationService); this._register(this._notebookOptions); + this._viewContext = new ViewContext(this._notebookOptions, new NotebookEventDispatcher()); this._overlayContainer = document.createElement('div'); this.scopedContextKeyService = contextKeyService.createScoped(this._overlayContainer); @@ -367,11 +367,15 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor this._updateForNotebookConfiguration(); } - if (e.compactView) { + if (e.compactView || e.focusIndicator || e.insertToolbarPosition) { this._styleElement?.remove(); this._createLayoutStyles(); this._webview?.updateOptions(this.notebookOptions.computeWebviewOptions()); } + + if (this._dimension && this._isVisible) { + this.layout(this._dimension); + } })); this.notebookEditorService.addNotebookEditor(this); @@ -552,10 +556,12 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor codeCellLeftMargin, markdownCellBottomMargin, markdownCellTopMargin, - bottomCellToolbarGap, - bottomCellToolbarHeight, + bottomToolbarGap: bottomCellToolbarGap, + bottomToolbarHeight: bottomCellToolbarHeight, collapsedIndicatorHeight, - compactView + compactView, + focusIndicator, + insertToolbarPosition } = this._notebookOptions.getLayoutConfiguration(); const styleSheets: string[] = []; @@ -566,6 +572,103 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor styleSheets.push(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .markdown-cell-row div.cell.code { margin-left: ${codeCellLeftMargin}px; }`); } + // focus indicator + if (focusIndicator === 'border') { + styleSheets.push(` + .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-top:before, + .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-bottom:before, + .monaco-workbench .notebookOverlay .monaco-list .markdown-cell-row .cell-inner-container:before, + .monaco-workbench .notebookOverlay .monaco-list .markdown-cell-row .cell-inner-container:after { + content: ""; + position: absolute; + width: 100%; + height: 1px; + } + + .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-left:before, + .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-right:before { + content: ""; + position: absolute; + width: 1px; + height: 100%; + z-index: 10; + } + + /* top border */ + .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-top:before { + border-top: 1px solid transparent; + } + + /* left border */ + .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-left:before { + border-left: 1px solid transparent; + } + + /* bottom border */ + .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-bottom:before { + border-bottom: 1px solid transparent; + } + + /* right border */ + .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-right:before { + border-right: 1px solid transparent; + } + `); + + // left and right border margins + styleSheets.push(` + .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.code-cell-row.focused .cell-focus-indicator-left:before, + .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.code-cell-row.focused .cell-focus-indicator-right:before, + .monaco-workbench .notebookOverlay .monaco-list.selection-multiple .monaco-list-row.code-cell-row.selected .cell-focus-indicator-left:before, + .monaco-workbench .notebookOverlay .monaco-list.selection-multiple .monaco-list-row.code-cell-row.selected .cell-focus-indicator-right:before { + top: -${cellTopMargin}px; height: calc(100% + ${cellTopMargin + cellBottomMargin}px) + }`); + } else { + styleSheets.push(` + .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-left:before, + .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-right:before { + content: ""; + position: absolute; + width: 0px; + height: 100%; + z-index: 10; + } + + .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-left:before { + border-left: 1px solid transparent; + border-right: 1px solid transparent; + border-radius: 2px; + } + `); + + // left and right border margins + styleSheets.push(` + .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.code-cell-row.focused .cell-focus-indicator-left:before, + .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.code-cell-row.focused .cell-focus-indicator-right:before, + .monaco-workbench .notebookOverlay .monaco-list.selection-multiple .monaco-list-row.code-cell-row.selected .cell-focus-indicator-left:before, + .monaco-workbench .notebookOverlay .monaco-list.selection-multiple .monaco-list-row.code-cell-row.selected .cell-focus-indicator-right:before { + top: 0px; height: 100%px; + }`); + } + + // between cell insert toolbar + if (insertToolbarPosition === 'betweenCells' || insertToolbarPosition === 'both') { + styleSheets.push(`.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container { display: flex; }`); + styleSheets.push(`.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .cell-list-top-cell-toolbar-container { display: flex; }`); + } else { + styleSheets.push(`.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container { display: none; }`); + styleSheets.push(`.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .cell-list-top-cell-toolbar-container { display: none; }`); + } + + // top insert toolbar + const topInsertToolbarHeight = this._notebookOptions.computeTopInserToolbarHeight(); + styleSheets.push(`.notebookOverlay .cell-list-top-cell-toolbar-container { top: -${topInsertToolbarHeight}px }`); + styleSheets.push(`.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element, + .notebookOverlay > .cell-list-container > .notebook-gutter > .monaco-list > .monaco-scrollable-element { + padding-top: ${topInsertToolbarHeight}px; + box-sizing: border-box; + }`); + styleSheets.push(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .code-cell-row div.cell.code { margin-left: ${codeCellLeftMargin + cellRunGutter}px; }`); styleSheets.push(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row div.cell { margin-right: ${cellRightMargin}px; }`); styleSheets.push(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row > .cell-inner-container { padding-top: ${cellTopMargin}px; }`); @@ -591,7 +694,6 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor styleSheets.push(`.notebookOverlay .monaco-list .monaco-list-row .cell-shadow-container-bottom { top: ${cellBottomMargin}px; }`); styleSheets.push(`.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-collapsed-part { margin-left: ${codeCellLeftMargin + cellRunGutter}px; height: ${collapsedIndicatorHeight}px; }`); - styleSheets.push(`.notebookOverlay .cell-list-top-cell-toolbar-container { top: -${SCROLLABLE_ELEMENT_PADDING_TOP}px }`); styleSheets.push(`.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container { height: ${bottomCellToolbarHeight}px }`); styleSheets.push(`.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .cell-list-top-cell-toolbar-container { height: ${bottomCellToolbarHeight}px }`); @@ -607,15 +709,6 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor display: none; }`); - // left and right border margins - styleSheets.push(` - .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.code-cell-row.focused .cell-focus-indicator-left:before, - .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.code-cell-row.focused .cell-focus-indicator-right:before, - .monaco-workbench .notebookOverlay .monaco-list.selection-multiple .monaco-list-row.code-cell-row.selected .cell-focus-indicator-left:before, - .monaco-workbench .notebookOverlay .monaco-list.selection-multiple .monaco-list-row.code-cell-row.selected .cell-focus-indicator-right:before { - top: -${cellTopMargin}px; height: calc(100% + ${cellTopMargin + cellBottomMargin}px) - }`); - this._styleElement.textContent = styleSheets.join('\n'); } @@ -641,6 +734,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor 'NotebookCellList', this._overlayContainer, this._body, + this._viewContext, this._listDelegate, renderers, this.scopedContextKeyService, @@ -1023,8 +1117,6 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor private async _attachModel(textModel: NotebookTextModel, viewState: INotebookEditorViewState | undefined) { await this._createWebview(this.getId(), textModel.uri); - - this._viewContext = new ViewContext(this._notebookOptions, new NotebookEventDispatcher()); this.viewModel = this.instantiationService.createInstance(NotebookViewModel, textModel.viewType, textModel, this._viewContext, this.getLayoutInfo()); this._viewContext.eventDispatcher.emit([new NotebookLayoutChangedEvent({ width: true, fontInfo: true }, this.getLayoutInfo())]); @@ -1125,7 +1217,8 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor })); if (this._dimension) { - this._list.layout(this._dimension.height - SCROLLABLE_ELEMENT_PADDING_TOP, this._dimension.width); + const topInserToolbarHeight = this._notebookOptions.computeTopInserToolbarHeight(); + this._list.layout(this._dimension.height - topInserToolbarHeight, this._dimension.width); } else { this._list.layout(); } @@ -1353,16 +1446,18 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor }; } + const topInserToolbarHeight = this._notebookOptions.computeTopInserToolbarHeight(); + this._dimension = new DOM.Dimension(dimension.width, dimension.height); DOM.size(this._body, dimension.width, dimension.height - (this._notebookTopToolbar?.useGlobalToolbar ? /** Toolbar height */ 26 : 0)); - if (this._list.getRenderHeight() < dimension.height - SCROLLABLE_ELEMENT_PADDING_TOP) { + if (this._list.getRenderHeight() < dimension.height - topInserToolbarHeight) { // the new dimension is larger than the list viewport, update its additional height first, otherwise the list view will move down a bit (as the `scrollBottom` will move down) - this._list.updateOptions({ additionalScrollHeight: this._scrollBeyondLastLine ? Math.max(0, (dimension.height - SCROLLABLE_ELEMENT_PADDING_TOP - 50)) : SCROLLABLE_ELEMENT_PADDING_TOP }); - this._list.layout(dimension.height - SCROLLABLE_ELEMENT_PADDING_TOP, dimension.width); + this._list.updateOptions({ additionalScrollHeight: this._scrollBeyondLastLine ? Math.max(0, (dimension.height - topInserToolbarHeight - 50)) : topInserToolbarHeight }); + this._list.layout(dimension.height - topInserToolbarHeight, dimension.width); } else { // the new dimension is smaller than the list viewport, if we update the additional height, the `scrollBottom` will move up, which moves the whole list view upwards a bit. So we run a layout first. - this._list.layout(dimension.height - SCROLLABLE_ELEMENT_PADDING_TOP, dimension.width); - this._list.updateOptions({ additionalScrollHeight: this._scrollBeyondLastLine ? Math.max(0, (dimension.height - SCROLLABLE_ELEMENT_PADDING_TOP - 50)) : SCROLLABLE_ELEMENT_PADDING_TOP }); + this._list.layout(dimension.height - topInserToolbarHeight, dimension.width); + this._list.updateOptions({ additionalScrollHeight: this._scrollBeyondLastLine ? Math.max(0, (dimension.height - topInserToolbarHeight - 50)) : topInserToolbarHeight }); } this._overlayContainer.style.visibility = 'visible'; @@ -1953,19 +2048,38 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor } } - toggleNotebookCellSelection(cell: ICellViewModel): void { + toggleNotebookCellSelection(selectedCell: ICellViewModel, selectFromPrevious: boolean): void { const currentSelections = this._list.getSelectedElements(); + const isSelected = currentSelections.includes(selectedCell); - const isSelected = currentSelections.includes(cell); + const previousSelection = selectFromPrevious ? currentSelections[currentSelections.length - 1] ?? selectedCell : selectedCell; + const selectedIndex = this._list.getViewIndex(selectedCell)!; + const previousIndex = this._list.getViewIndex(previousSelection)!; + + const cellsInSelectionRange = this.getCellsInRange(selectedIndex, previousIndex); if (isSelected) { // Deselect - this._list.selectElements(currentSelections.filter(current => current !== cell)); + this._list.selectElements(currentSelections.filter(current => !cellsInSelectionRange.includes(current))); } else { // Add to selection - this._list.selectElements([...currentSelections, cell]); + this.focusElement(selectedCell); + this._list.selectElements([...currentSelections.filter(current => !cellsInSelectionRange.includes(current)), ...cellsInSelectionRange]); } } + private getCellsInRange(fromInclusive: number, toInclusive: number): ICellViewModel[] { + const selectedCellsInRange: ICellViewModel[] = []; + for (let index = 0; index < this._list.length; ++index) { + const cell = this._list.element(index); + if (cell) { + if ((index >= fromInclusive && index <= toInclusive) || (index >= toInclusive && index <= fromInclusive)) { + selectedCellsInRange.push(cell); + } + } + } + return selectedCellsInRange; + } + focusNotebookCell(cell: ICellViewModel, focusItem: 'editor' | 'container' | 'output', options?: IFocusNotebookCellOptions) { if (this._isDisposed) { return; @@ -2315,8 +2429,8 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor const cell = this.getCellById(cellId); const layoutConfiguration = this._notebookOptions.getLayoutConfiguration(); if (cell && cell instanceof MarkdownCellViewModel) { - if (height + layoutConfiguration.bottomCellToolbarGap !== cell.layoutInfo.totalHeight) { - this._debug('updateMarkdownCellHeight', cell.handle, height + layoutConfiguration.bottomCellToolbarGap, isInit); + if (height + layoutConfiguration.bottomToolbarGap !== cell.layoutInfo.totalHeight) { + this._debug('updateMarkdownCellHeight', cell.handle, height + layoutConfiguration.bottomToolbarGap, isInit); cell.renderedMarkdownHeight = height; } } @@ -2329,24 +2443,24 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor } } - markdownCellDragStart(cellId: string, ctx: { clientY: number }): void { + markdownCellDragStart(cellId: string, event: { dragOffsetY: number }): void { const cell = this.getCellById(cellId); if (cell instanceof MarkdownCellViewModel) { - this._dndController?.startExplicitDrag(cell, ctx); + this._dndController?.startExplicitDrag(cell, event.dragOffsetY); } } - markdownCellDrag(cellId: string, ctx: { clientY: number }): void { + markdownCellDrag(cellId: string, event: { dragOffsetY: number }): void { const cell = this.getCellById(cellId); if (cell instanceof MarkdownCellViewModel) { - this._dndController?.explicitDrag(cell, ctx); + this._dndController?.explicitDrag(cell, event.dragOffsetY); } } - markdownCellDrop(cellId: string, ctx: { clientY: number, ctrlKey: boolean, altKey: boolean }): void { + markdownCellDrop(cellId: string, event: { dragOffsetY: number, ctrlKey: boolean, altKey: boolean }): void { const cell = this.getCellById(cellId); if (cell instanceof MarkdownCellViewModel) { - this._dndController?.explicitDrop(cell, ctx); + this._dndController?.explicitDrop(cell, event); } } @@ -2390,7 +2504,6 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor this._webviewTransparentCover = null; this._dndController = null; this._listTopCellToolbar = null; - this._viewContext = undefined; this._notebookViewModel = undefined; this._cellContextKeyManager = null; this._renderedEditors.clear(); @@ -2536,12 +2649,6 @@ export const cellEditorBackground = registerColor('notebook.cellEditorBackground }, nls.localize('notebook.cellEditorBackground', "Cell editor background color.")); registerThemingParticipant((theme, collector) => { - collector.addRule(`.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element, - .notebookOverlay > .cell-list-container > .notebook-gutter > .monaco-list > .monaco-scrollable-element { - padding-top: ${SCROLLABLE_ELEMENT_PADDING_TOP}px; - box-sizing: border-box; - }`); - const link = theme.getColor(textLinkForeground); if (link) { collector.addRule(`.notebookOverlay .output a, @@ -2609,8 +2716,8 @@ registerThemingParticipant((theme, collector) => { const focusedCellBackgroundColor = theme.getColor(focusedCellBackground); if (focusedCellBackgroundColor) { - collector.addRule(`.notebookOverlay .code-cell-row.focused .cell-focus-indicator, - .notebookOverlay .markdown-cell-row.focused { background-color: ${focusedCellBackgroundColor} !important; }`); + collector.addRule(`.notebookOverlay .code-cell-row.focused .cell-focus-indicator { background-color: ${focusedCellBackgroundColor} !important; }`); + collector.addRule(`.notebookOverlay .markdown-cell-row.focused { background-color: ${focusedCellBackgroundColor} !important; }`); collector.addRule(`.notebookOverlay .code-cell-row.focused .cell-collapsed-part { background-color: ${focusedCellBackgroundColor} !important; }`); } diff --git a/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts b/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts index f14f3fcca8d..eec47318496 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts @@ -24,8 +24,8 @@ import { CellViewModel, NotebookViewModel } from 'vs/workbench/contrib/notebook/ import { diff, NOTEBOOK_EDITOR_CURSOR_BOUNDARY, CellKind, SelectionStateType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { ICellRange, cellRangesToIndexes } from 'vs/workbench/contrib/notebook/common/notebookRange'; import { clamp } from 'vs/base/common/numbers'; -import { SCROLLABLE_ELEMENT_PADDING_TOP } from 'vs/workbench/contrib/notebook/browser/constants'; import { ISplice } from 'vs/base/common/sequence'; +import { ViewContext } from 'vs/workbench/contrib/notebook/browser/viewModel/viewContext'; export interface IFocusNextPreviousDelegate { onFocusNext(applyFocusNext: () => void): void; @@ -91,10 +91,13 @@ export class NotebookCellList extends WorkbenchList implements ID private readonly _focusNextPreviousDelegate: IFocusNextPreviousDelegate; + private readonly _viewContext: ViewContext; + constructor( private listUser: string, parentContainer: HTMLElement, container: HTMLElement, + viewContext: ViewContext, delegate: IListVirtualDelegate, renderers: IListRenderer[], contextKeyService: IContextKeyService, @@ -106,6 +109,7 @@ export class NotebookCellList extends WorkbenchList implements ID ) { super(listUser, container, delegate, renderers, options, contextKeyService, listService, themeService, configurationService, keybindingService); NOTEBOOK_CELL_LIST_FOCUSED.bindTo(this.contextKeyService).set(true); + this._viewContext = viewContext; this._focusNextPreviousDelegate = options.focusNextPreviousDelegate; this._previousFocusedElements = this.getFocusedElements(); this._localDisposableStore.add(this.onDidChangeFocus((e) => { @@ -900,7 +904,8 @@ export class NotebookCellList extends WorkbenchList implements ID } getViewScrollBottom() { - return this.getViewScrollTop() + this.view.renderHeight - SCROLLABLE_ELEMENT_PADDING_TOP; + const topInsertToolbarHeight = this._viewContext.notebookOptions.computeTopInserToolbarHeight(); + return this.getViewScrollTop() + this.view.renderHeight - topInsertToolbarHeight; } private _revealRange(viewIndex: number, range: Range, revealType: CellRevealType, newlyCreated: boolean, alignToBottom: boolean) { diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts index 0a0ce408a70..157cb43f9c2 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts @@ -26,10 +26,10 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { asWebviewUri } from 'vs/workbench/api/common/shared/webview'; import { CellEditState, ICellOutputViewModel, ICommonCellInfo, ICommonNotebookEditor, IDisplayOutputLayoutUpdateRequest, IDisplayOutputViewModel, IGenericCellViewModel, IInsetRenderOutput, RenderOutputType } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { preloadsScriptStr, WebviewPreloadRenderer } from 'vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads'; +import { preloadsScriptStr, RendererMetadata } from 'vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads'; import { transformWebviewThemeVars } from 'vs/workbench/contrib/notebook/browser/view/renderers/webviewThemeMapping'; import { MarkdownCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/markdownCellViewModel'; -import { INotebookKernel, INotebookRendererInfo, NotebookRendererMatch } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { INotebookKernel, INotebookRendererInfo } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { IWebviewService, WebviewContentPurpose, WebviewElement } from 'vs/workbench/contrib/webview/browser/webview'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; @@ -131,17 +131,13 @@ export interface IToggleMarkdownPreviewMessage extends BaseToWebviewMessage { export interface ICellDragStartMessage extends BaseToWebviewMessage { type: 'cell-drag-start'; readonly cellId: string; - readonly position: { - readonly clientY: number; - }; + readonly dragOffsetY: number; } export interface ICellDragMessage extends BaseToWebviewMessage { type: 'cell-drag'; readonly cellId: string; - readonly position: { - readonly clientY: number; - }; + readonly dragOffsetY: number; } export interface ICellDropMessage extends BaseToWebviewMessage { @@ -149,9 +145,7 @@ export interface ICellDropMessage extends BaseToWebviewMessage { readonly cellId: string; readonly ctrlKey: boolean readonly altKey: boolean; - readonly position: { - readonly clientY: number; - }; + readonly dragOffsetY: number; } export interface ICellDragEndMessage extends BaseToWebviewMessage { @@ -203,7 +197,7 @@ export interface ICreationRequestMessage { cellTop: number; outputOffset: number; left: number; - requiredPreloads: ReadonlyArray; + requiredPreloads: ReadonlyArray; readonly initiallyHidden?: boolean; rendererId?: string | undefined; } @@ -263,17 +257,15 @@ export interface IAckOutputHeightMessage { height: number; } -export type PreloadSource = 'kernel' | { rendererId: string }; -export interface IPreloadResource { +export interface IControllerPreload { originalUri: string; uri: string; - source: PreloadSource; } -export interface IUpdatePreloadResourceMessage { +export interface IUpdateControllerPreloadsMessage { type: 'preload'; - resources: IPreloadResource[]; + resources: IControllerPreload[]; } export interface IUpdateDecorationsMessage { @@ -376,7 +368,7 @@ export type ToWebviewMessage = | IClearOutputRequestMessage | IHideOutputMessage | IShowOutputMessage - | IUpdatePreloadResourceMessage + | IUpdateControllerPreloadsMessage | IUpdateDecorationsMessage | ICustomKernelMessage | ICreateMarkdownMessage @@ -496,7 +488,7 @@ export class BackLayerWebView extends Disposable { } private generateContent(coreDependencies: string, baseUrl: string) { - const markupRenderer = this.getMarkdownRenderer(); + const renderersData = this.getRendererData(); return html` @@ -755,36 +747,19 @@ export class BackLayerWebView extends Disposable { ${coreDependencies}
- + `; } - private getMarkdownRenderer(): WebviewPreloadRenderer[] { - const markdownMimeType = 'text/markdown'; - const allRenderers = this.notebookService.getRenderers() - .filter(renderer => renderer.matchesWithoutKernel(markdownMimeType) !== NotebookRendererMatch.Never); - - const topLevelMarkdownRenderers = allRenderers - .filter(renderer => renderer.dependencies.length === 0); - - const subRenderers = new Map>(); - for (const renderer of allRenderers) { - for (const dep of renderer.dependencies) { - if (!subRenderers.has(dep)) { - subRenderers.set(dep, []); - } - const entryPoint = this.asWebviewUri(renderer.entrypoint, renderer.extensionLocation); - subRenderers.get(dep)!.push({ entrypoint: entryPoint.toString(true) }); - } - } - - return topLevelMarkdownRenderers.map((renderer): WebviewPreloadRenderer => { - const src = this.asWebviewUri(renderer.entrypoint, renderer.extensionLocation); + private getRendererData(): RendererMetadata[] { + return this.notebookService.getRenderers().map((renderer): RendererMetadata => { + const entrypoint = this.asWebviewUri(renderer.entrypoint, renderer.extensionLocation).toString(); return { - entrypoint: src.toString(), + id: renderer.id, + entrypoint, mimeTypes: renderer.mimeTypes, - dependencies: subRenderers.get(renderer.id) || [], + extends: renderer.extends, }; }); } @@ -1025,8 +1000,8 @@ var requirejs = (function() { const cell = this.notebookEditor.getCellById(data.cellId); if (cell) { if (data.shiftKey || (isMacintosh ? data.metaKey : data.ctrlKey)) { - // Add to selection - this.notebookEditor.toggleNotebookCellSelection(cell); + // Modify selection + this.notebookEditor.toggleNotebookCellSelection(cell, /* fromPrevious */ data.shiftKey); } else { // Normal click this.notebookEditor.focusNotebookCell(cell, 'container', { skipReveal: true }); @@ -1086,18 +1061,18 @@ var requirejs = (function() { } case 'cell-drag-start': { - this.notebookEditor.markdownCellDragStart(data.cellId, data.position); + this.notebookEditor.markdownCellDragStart(data.cellId, data); break; } case 'cell-drag': { - this.notebookEditor.markdownCellDrag(data.cellId, data.position); + this.notebookEditor.markdownCellDrag(data.cellId, data); break; } case 'cell-drop': { this.notebookEditor.markdownCellDrop(data.cellId, { - clientY: data.position.clientY, + dragOffsetY: data.dragOffsetY, ctrlKey: data.ctrlKey, altKey: data.altKey, }); @@ -1211,7 +1186,6 @@ var requirejs = (function() { if (this._currentKernel) { this._updatePreloadsFromKernel(this._currentKernel); } - this.updateRendererPreloads(renderers); for (const [output, inset] of this.insetMapping.entries()) { this._sendMessageToWebview({ ...inset.cachedCreation, initiallyHidden: this.hiddenInsetMapping.has(output) }); @@ -1486,7 +1460,6 @@ var requirejs = (function() { ...messageBase, outputId: output.outputId, rendererId: content.renderer.id, - requiredPreloads: await this.updateRendererPreloads([content.renderer]), content: { type: RenderOutputType.Extension, outputId: output.outputId, @@ -1617,13 +1590,13 @@ var requirejs = (function() { } private _updatePreloadsFromKernel(kernel: INotebookKernel) { - const resources: IPreloadResource[] = []; + const resources: IControllerPreload[] = []; for (const preload of kernel.preloadUris) { const uri = this.environmentService.isExtensionDevelopment && (preload.scheme === 'http' || preload.scheme === 'https') ? preload : this.asWebviewUri(preload, undefined); if (!this._preloadsCache.has(uri.toString())) { - resources.push({ uri: uri.toString(), originalUri: preload.toString(), source: 'kernel' }); + resources.push({ uri: uri.toString(), originalUri: preload.toString() }); this._preloadsCache.add(uri.toString()); } } @@ -1635,43 +1608,7 @@ var requirejs = (function() { this._updatePreloads(resources); } - async updateRendererPreloads(renderers: Iterable) { - if (this._disposed) { - return []; - } - - const requiredPreloads: IPreloadResource[] = []; - const resources: IPreloadResource[] = []; - const extensionLocations: URI[] = []; - for (const rendererInfo of renderers) { - extensionLocations.push(rendererInfo.extensionLocation); - for (const preload of [rendererInfo.entrypoint, ...rendererInfo.preloads]) { - const uri = this.asWebviewUri(preload, rendererInfo.extensionLocation); - const resource: IPreloadResource = { - uri: uri.toString(), - originalUri: preload.toString(), - source: { rendererId: rendererInfo.id }, - }; - - requiredPreloads.push(resource); - - if (!this._preloadsCache.has(uri.toString())) { - resources.push(resource); - this._preloadsCache.add(uri.toString()); - } - } - } - - if (!resources.length) { - return requiredPreloads; - } - - this.rendererRootsCache = extensionLocations; - this._updatePreloads(resources); - return requiredPreloads; - } - - private _updatePreloads(resources: IPreloadResource[]) { + private _updatePreloads(resources: IControllerPreload[]) { if (!this.webview) { return; } diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellDnd.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellDnd.ts index fca5bd43ce6..03eea9d8ec5 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellDnd.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellDnd.ts @@ -157,7 +157,7 @@ export class CellDragAndDropController extends Disposable { private updateInsertIndicator(dropDirection: string, insertionIndicatorAbsolutePos: number) { const layoutInfo = this.notebookEditor.notebookOptions.getLayoutConfiguration(); - const insertionIndicatorTop = insertionIndicatorAbsolutePos - this.list.scrollTop + layoutInfo.bottomCellToolbarGap / 2; + const insertionIndicatorTop = insertionIndicatorAbsolutePos - this.list.scrollTop + layoutInfo.bottomToolbarGap / 2; if (insertionIndicatorTop >= 0) { this.listInsertionIndicator.style.top = `${insertionIndicatorTop}px`; this.setInsertIndicatorVisibility(true); @@ -195,12 +195,12 @@ export class CellDragAndDropController extends Disposable { } } - private _dropImpl(draggedCell: ICellViewModel, dropDirection: 'above' | 'below', ctx: { clientY: number, ctrlKey: boolean, altKey: boolean }, draggedOverCell: ICellViewModel) { + private _dropImpl(draggedCell: ICellViewModel, dropDirection: 'above' | 'below', ctx: { ctrlKey: boolean, altKey: boolean }, draggedOverCell: ICellViewModel) { const cellTop = this.list.getAbsoluteTopOfElement(draggedOverCell); const cellHeight = this.list.elementHeight(draggedOverCell); const insertionIndicatorAbsolutePos = dropDirection === 'above' ? cellTop : cellTop + cellHeight; const layoutInfo = this.notebookEditor.notebookOptions.getLayoutConfiguration(); - const insertionIndicatorTop = insertionIndicatorAbsolutePos - this.list.scrollTop + layoutInfo.bottomCellToolbarGap / 2; + const insertionIndicatorTop = insertionIndicatorAbsolutePos - this.list.scrollTop + layoutInfo.bottomToolbarGap / 2; const editorHeight = this.notebookEditor.getDomNode().getBoundingClientRect().height; if (insertionIndicatorTop < 0 || insertionIndicatorTop > editorHeight) { // Ignore drop, insertion point is off-screen @@ -323,18 +323,18 @@ export class CellDragAndDropController extends Disposable { })); } - public startExplicitDrag(cell: ICellViewModel, position: { clientY: number }) { + public startExplicitDrag(cell: ICellViewModel, _dragOffsetY: number) { this.currentDraggedCell = cell; this.setInsertIndicatorVisibility(true); } - public explicitDrag(cell: ICellViewModel, position: { clientY: number }) { - const target = this.list.elementAt(position.clientY); + public explicitDrag(cell: ICellViewModel, dragOffsetY: number) { + const target = this.list.elementAt(dragOffsetY); if (target && target !== cell) { const cellTop = this.list.getAbsoluteTopOfElement(target); const cellHeight = this.list.elementHeight(target); - const dropDirection = this.getExplicitDragDropDirection(position.clientY, cellTop, cellHeight); + const dropDirection = this.getExplicitDragDropDirection(dragOffsetY, cellTop, cellHeight); const insertionIndicatorAbsolutePos = dropDirection === 'above' ? cellTop : cellTop + cellHeight; this.updateInsertIndicator(dropDirection, insertionIndicatorAbsolutePos); } @@ -344,16 +344,19 @@ export class CellDragAndDropController extends Disposable { return; } - const viewRect = this.notebookEditor.getDomNode().getBoundingClientRect(); - const eventPositionInView = position.clientY - this.list.scrollTop; + const notebookViewRect = this.notebookEditor.getDomNode().getBoundingClientRect(); + const eventPositionInView = dragOffsetY - this.list.scrollTop; - const scrollMargin = 0.2; - const maxScrollPerFrame = 20; - const eventPositionRatio = eventPositionInView / viewRect.height; - if (eventPositionRatio < scrollMargin) { - this.list.scrollTop -= maxScrollPerFrame * (1 - eventPositionRatio / scrollMargin); - } else if (eventPositionRatio > 1 - scrollMargin) { - this.list.scrollTop += maxScrollPerFrame * (1 - ((1 - eventPositionRatio) / scrollMargin)); + // Percentage from the top/bottom of the screen where we start scrolling while dragging + const notebookViewScrollMargins = 0.2; + + const maxScrollDeltaPerFrame = 20; + + const eventPositionRatio = eventPositionInView / notebookViewRect.height; + if (eventPositionRatio < notebookViewScrollMargins) { + this.list.scrollTop -= maxScrollDeltaPerFrame * (1 - eventPositionRatio / notebookViewScrollMargins); + } else if (eventPositionRatio > 1 - notebookViewScrollMargins) { + this.list.scrollTop += maxScrollDeltaPerFrame * (1 - ((1 - eventPositionRatio) / notebookViewScrollMargins)); } } @@ -361,24 +364,23 @@ export class CellDragAndDropController extends Disposable { this.setInsertIndicatorVisibility(false); } - public explicitDrop(cell: ICellViewModel, ctx: { clientY: number, ctrlKey: boolean, altKey: boolean }) { + public explicitDrop(cell: ICellViewModel, ctx: { dragOffsetY: number, ctrlKey: boolean, altKey: boolean }) { this.currentDraggedCell = undefined; this.setInsertIndicatorVisibility(false); - const target = this.list.elementAt(ctx.clientY); + const target = this.list.elementAt(ctx.dragOffsetY); if (!target || target === cell) { return; } const cellTop = this.list.getAbsoluteTopOfElement(target); const cellHeight = this.list.elementHeight(target); - const dropDirection = this.getExplicitDragDropDirection(ctx.clientY, cellTop, cellHeight); + const dropDirection = this.getExplicitDragDropDirection(ctx.dragOffsetY, cellTop, cellHeight); this._dropImpl(cell, dropDirection, ctx, target); } private getExplicitDragDropDirection(clientY: number, cellTop: number, cellHeight: number) { - const dragOffset = this.list.scrollTop + clientY; - const dragPosInElement = dragOffset - cellTop; + const dragPosInElement = clientY - cellTop; const dragPosRatio = dragPosInElement / cellHeight; return this.getDropInsertDirection(dragPosRatio); diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts index 23f94176202..555c743706c 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts @@ -881,10 +881,10 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende const layoutInfo = this.notebookEditor.notebookOptions.getLayoutConfiguration(); templateData.focusIndicatorLeft.style.height = `${element.layoutInfo.indicatorHeight}px`; templateData.focusIndicatorRight.style.height = `${element.layoutInfo.indicatorHeight}px`; - templateData.focusIndicatorBottom.style.top = `${element.layoutInfo.totalHeight - layoutInfo.bottomCellToolbarGap - layoutInfo.cellBottomMargin}px`; + templateData.focusIndicatorBottom.style.top = `${element.layoutInfo.totalHeight - layoutInfo.bottomToolbarGap - layoutInfo.cellBottomMargin}px`; templateData.outputContainer.style.top = `${element.layoutInfo.outputContainerOffset}px`; templateData.outputShowMoreContainer.style.top = `${element.layoutInfo.outputShowMoreContainerOffset}px`; - templateData.dragHandle.style.height = `${element.layoutInfo.totalHeight - layoutInfo.bottomCellToolbarGap}px`; + templateData.dragHandle.style.height = `${element.layoutInfo.totalHeight - layoutInfo.bottomToolbarGap}px`; } renderElement(element: CodeCellViewModel, index: number, templateData: CodeCellRenderTemplate, height: number | undefined): void { diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts index 282608bfea7..35609e1368b 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts @@ -41,7 +41,7 @@ interface PreloadStyles { declare function __import(path: string): Promise; -async function webviewPreloads(style: PreloadStyles, rendererData: readonly WebviewPreloadRenderer[]) { +async function webviewPreloads(style: PreloadStyles, rendererData: readonly RendererMetadata[]) { const acquireVsCodeApi = globalThis.acquireVsCodeApi; const vscode = acquireVsCodeApi(); delete (globalThis as any).acquireVsCodeApi; @@ -111,32 +111,94 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Webv } }; - const runScript = async (url: string, originalUri: string, globals: { [name: string]: unknown } = {}): Promise<() => (PreloadResult)> => { - let text: string; - try { - const res = await fetch(url); - text = await res.text(); - if (!res.ok) { - throw new Error(`Unexpected ${res.status} requesting ${originalUri}: ${text || res.statusText}`); - } - - globals.scriptUrl = url; - } catch (e) { - return () => ({ state: PreloadState.Error, error: e.message }); + async function loadScriptSource(url: string, originalUri = url): Promise { + const res = await fetch(url); + const text = await res.text(); + if (!res.ok) { + throw new Error(`Unexpected ${res.status} requesting ${originalUri}: ${text || res.statusText}`); } + return text; + } + + interface RendererContext { + getState(): T | undefined; + setState(newState: T): void; + + getRenderer(id: string): any | undefined; + } + + function createRendererContext(rendererId: string): RendererContext { + return { + setState: newState => vscode.setState({ ...vscode.getState(), [rendererId]: newState }), + getState: () => { + const state = vscode.getState(); + return typeof state === 'object' && state ? state[rendererId] as T : undefined; + }, + getRenderer: (id: string) => renderers.getRenderer(id), + }; + } + + interface ScriptModule { + activate: (ctx?: RendererContext) => any; + } + + const invokeSourceWithGlobals = (functionSrc: string, globals: { [name: string]: unknown }) => { const args = Object.entries(globals); - return () => { - try { - new Function(...args.map(([k]) => k), text)(...args.map(([, v]) => v)); - return { state: PreloadState.Ok }; - } catch (e) { - console.error(e); - return { state: PreloadState.Error, error: e.message }; + return new Function(...args.map(([k]) => k), functionSrc)(...args.map(([, v]) => v)); + }; + + const runPreload = async (url: string, originalUri: string): Promise => { + const text = await loadScriptSource(url, originalUri); + return { + activate: () => { + return invokeSourceWithGlobals(text, kernelPreloadGlobals); } }; }; + const runRenderScript = async (url: string, rendererId: string): Promise => { + const text = await loadScriptSource(url); + // TODO: Support both the new module based renderers and the old style global renderers + const isModule = /\bexport\b.*\bactivate\b/.test(text); + if (isModule) { + return __import(url); + } else { + return createBackCompatModule(rendererId, url, text); + } + }; + + const createBackCompatModule = (rendererId: string, scriptUrl: string, scriptText: string): ScriptModule => ({ + activate: (): RendererApi => { + const onDidCreateOutput = createEmitter(); + const onWillDestroyOutput = createEmitter(); + + const globals = { + scriptUrl, + acquireNotebookRendererApi: (): GlobalNotebookRendererApi => ({ + onDidCreateOutput: onDidCreateOutput.event, + onWillDestroyOutput: onWillDestroyOutput.event, + setState: newState => vscode.setState({ ...vscode.getState(), [rendererId]: newState }), + getState: () => { + const state = vscode.getState(); + return typeof state === 'object' && state ? state[rendererId] as T : undefined; + }, + }), + }; + + invokeSourceWithGlobals(scriptText, globals); + + return { + renderCell(id, context) { + onDidCreateOutput.fire({ ...context, outputId: id }); + }, + destroyCell(id) { + onWillDestroyOutput.fire(id ? { outputId: id } : undefined); + } + }; + } + }); + const dimensionUpdater = new class { private readonly pending = new Map(); @@ -352,8 +414,6 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Webv focusTrackers.set(outputId, new FocusTracker(element, outputId)); } - const dontEmit = Symbol('dontEmit'); - function createEmitter(listenerChange: (listeners: Set>) => void = () => undefined): EmitterLike { const listeners = new Set>(); return { @@ -385,29 +445,21 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Webv }; } - // Maps the events in the given emitter, invoking mapFn on each one. mapFn can return - // the dontEmit symbol to skip emission. - function mapEmitter(emitter: EmitterLike, mapFn: (data: T) => R | typeof dontEmit) { - let listener: IDisposable; - const mapped = createEmitter(listeners => { - if (listeners.size && !listener) { - listener = emitter.event(data => { - const v = mapFn(data); - if (v !== dontEmit) { - mapped.fire(v); - } - }); - } else if (listener && !listeners.size) { - listener.dispose(); - } - }); - - return mapped.event; + function showPreloadErrors(outputNode: HTMLElement, ...errors: readonly Error[]) { + outputNode.innerText = `Error loading preloads:`; + const errList = document.createElement('ul'); + for (const result of errors) { + console.error(result); + const item = document.createElement('li'); + item.innerText = result.message; + errList.appendChild(item); + } + outputNode.appendChild(errList); } interface ICreateCellInfo { element: HTMLElement; - outputId: string; + outputId?: string; mime: string; value: unknown; @@ -418,26 +470,15 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Webv outputId: string; } - const onWillDestroyOutput = createEmitter<'all' | { rendererId: string, info: IDestroyCellInfo }>(); - const onDidCreateOutput = createEmitter<{ rendererId: string, info: ICreateCellInfo }>(); const onDidReceiveKernelMessage = createEmitter(); - const acquireNotebookRendererApi = (id: string) => ({ - setState(newState: T) { - vscode.setState({ ...vscode.getState(), [id]: newState }); - }, - getState(): T | undefined { - const state = vscode.getState(); - return typeof state === 'object' && state ? state[id] as T : undefined; - }, - onWillDestroyOutput: mapEmitter(onWillDestroyOutput, (evt) => { - if (evt === 'all') { - return undefined; - } - return evt.rendererId === id ? evt.info : dontEmit; - }), - onDidCreateOutput: mapEmitter(onDidCreateOutput, ({ rendererId, info }) => rendererId === id ? info : dontEmit), - }); + /** @deprecated */ + interface GlobalNotebookRendererApi { + setState: (newState: T) => void; + getState(): T | undefined; + readonly onWillDestroyOutput: Event; + readonly onDidCreateOutput: Event; + } const kernelPreloadGlobals = { acquireVsCodeApi, @@ -445,42 +486,6 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Webv postKernelMessage: (data: unknown) => postNotebookMessage('customKernelMessage', { message: data }), }; - const enum PreloadState { - Ok, - Error - } - - type PreloadResult = { state: PreloadState.Ok } | { state: PreloadState.Error, error: string }; - - /** - * Map of preload resource URIs to promises that resolve one the resource - * loads or errors. - */ - const preloadPromises = new Map>(); - const queuedOuputActions = new Map>(); - - /** - * Enqueues an action that affects a output. This blocks behind renderer load - * requests that affect the same output. This should be called whenever you - * do something that affects output to ensure it runs in - * the correct order. - */ - const enqueueOutputAction = (event: T, fn: (event: T) => Promise | void) => { - const queued = queuedOuputActions.get(event.outputId); - const maybePromise = queued ? queued.then(() => fn(event)) : fn(event); - if (typeof maybePromise === 'undefined') { - return; // a synchonrously-called function, we're done - } - - const promise = maybePromise.then(() => { - if (queuedOuputActions.get(event.outputId) === promise) { - queuedOuputActions.delete(event.outputId); - } - }); - - queuedOuputActions.set(event.outputId, promise); - }; - const ttPolicy = window.trustedTypes?.createPolicy('notebookOutputRenderer', { createHTML: value => value, createScript: value => value, @@ -562,10 +567,15 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Webv } } break; - case 'html': - enqueueOutputAction(event.data, async data => { - const preloadResults = await Promise.all(data.requiredPreloads.map(p => preloadPromises.get(p.uri))); - if (!queuedOuputActions.has(data.outputId)) { // output was cleared while loading + case 'html': { + const data = event.data; + outputs.enqueue(event.data.outputId, async (state) => { + const preloadsAndErrors = await Promise.all([ + data.rendererId ? renderers.load(data.rendererId) : undefined, + ...data.requiredPreloads.map(p => kernelPreloads.waitFor(p.uri)), + ].map(p => p?.catch(err => err))); + + if (state.cancelled) { return; } @@ -615,37 +625,26 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Webv if (content.type === RenderOutputType.Html) { const trustedHtml = ttPolicy?.createHTML(content.htmlContent) ?? content.htmlContent; outputNode.innerHTML = trustedHtml as string; - cellOutputContainer.appendChild(outputContainer); - outputContainer.appendChild(outputNode); domEval(outputNode); - } else if (preloadResults.some(e => e?.state === PreloadState.Error)) { - outputNode.innerText = `Error loading preloads:`; - const errList = document.createElement('ul'); - for (const result of preloadResults) { - if (result?.state === PreloadState.Error) { - const item = document.createElement('li'); - item.innerText = result.error; - errList.appendChild(item); - } - } - outputNode.appendChild(errList); - cellOutputContainer.appendChild(outputContainer); - outputContainer.appendChild(outputNode); + } else if (preloadsAndErrors.some(e => e instanceof Error)) { + const errors = preloadsAndErrors.filter((e): e is Error => e instanceof Error); + showPreloadErrors(outputNode, ...errors); } else { - onDidCreateOutput.fire({ - rendererId: data.rendererId!, - info: { + const rendererApi = preloadsAndErrors[0] as RendererApi; + try { + rendererApi.renderCell(outputId, { element: outputNode, - outputId, mime: content.mimeType, value: content.value, metadata: content.metadata, - } - }); - cellOutputContainer.appendChild(outputContainer); - outputContainer.appendChild(outputNode); + }); + } catch (e) { + showPreloadErrors(outputNode, e); + } } + cellOutputContainer.appendChild(outputContainer); + outputContainer.appendChild(outputNode); resizeObserver.observe(outputNode, outputId, true); const clientHeight = outputNode.clientHeight; @@ -670,6 +669,7 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Webv cellOutputContainer.style.visibility = data.initiallyHidden ? 'hidden' : 'visible'; }); break; + } case 'view-scroll': { // const date = new Date(); @@ -696,8 +696,7 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Webv break; } case 'clear': - queuedOuputActions.clear(); // stop all loading outputs - onWillDestroyOutput.fire('all'); + renderers.clearAll(); document.getElementById('container')!.innerText = ''; focusTrackers.forEach(ft => { @@ -709,26 +708,29 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Webv const output = document.getElementById(event.data.outputId); const { rendererId, outputId } = event.data; - queuedOuputActions.delete(outputId); // stop any in-progress rendering + outputs.cancelOutput(outputId); if (output && output.parentNode) { if (rendererId) { - onWillDestroyOutput.fire({ rendererId, info: { outputId } }); + renderers.clearOutput(rendererId, outputId); } output.parentNode.removeChild(output); } break; } - case 'hideOutput': - enqueueOutputAction(event.data, ({ outputId }) => { + case 'hideOutput': { + const { outputId } = event.data; + outputs.enqueue(event.data.outputId, () => { const container = document.getElementById(outputId)?.parentElement?.parentElement; if (container) { container.style.visibility = 'hidden'; } }); break; - case 'showOutput': - enqueueOutputAction(event.data, ({ outputId, cellTop: top, }) => { + } + case 'showOutput': { + const { outputId, cellTop: top } = event.data; + outputs.enqueue(event.data.outputId, () => { const output = document.getElementById(outputId); if (output) { output.parentElement!.parentElement!.style.visibility = 'visible'; @@ -740,6 +742,7 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Webv } }); break; + } case 'ack-dimension': { const { outputId, height } = event.data; @@ -752,24 +755,8 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Webv } case 'preload': const resources = event.data.resources; - let queue: Promise = Promise.resolve({ state: PreloadState.Ok }); - for (const { uri, originalUri, source } of resources) { - const globals = source === 'kernel' - ? kernelPreloadGlobals - : { acquireNotebookRendererApi: () => acquireNotebookRendererApi(source.rendererId) }; - - // create the promise so that the scripts download in parallel, but - // only invoke them in series within the queue - const promise = runScript(uri, originalUri, globals); - queue = queue.then(() => promise.then(fn => { - const result = fn(); - if (result.state === PreloadState.Error) { - console.error(result.error); - } - - return result; - })); - preloadPromises.set(uri, queue); + for (const { uri, originalUri } of resources) { + kernelPreloads.load(uri, originalUri); } break; case 'focus-output': @@ -806,51 +793,193 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Webv } }); - interface MarkupRenderer { - renderMarkup: (context: { element: HTMLElement, content: string }) => void; + interface RendererApi { + renderCell: (id: string, context: ICreateCellInfo) => void; + destroyCell?: (id?: string) => void; } - const markupRenderers = new class { + class Renderer { + constructor( + public readonly data: RendererMetadata, + private readonly loadExtension: (id: string) => Promise, + ) { } - private readonly mimeTypesToRenderers = new Map Promise; - }>(); + private _loadPromise: Promise | undefined; + private _api: RendererApi | undefined; + + public get api() { return this._api; } + + public load(): Promise { + if (!this._loadPromise) { + this._loadPromise = this._load(); + } + + return this._loadPromise; + } + + /** Inner function cached in the _loadPromise(). */ + private async _load() { + const module = await runRenderScript(this.data.entrypoint, this.data.id); + if (!module) { + return; + } + + const api = module.activate(createRendererContext(this.data.id)); + this._api = api; + + // Squash any errors extends errors. They won't prevent the renderer + // itself from working, so just log them. + await Promise.all(rendererData + .filter(d => d.extends === this.data.id) + .map(d => this.loadExtension(d.id).catch(console.error)), + ); + + return api; + } + } + + const kernelPreloads = new class { + private readonly preloads = new Map>(); + + /** + * Returns a promise that resolves when the given preload is activated. + */ + public waitFor(uri: string) { + return this.preloads.get(uri) || Promise.resolve(new Error(`Preload not ready: ${uri}`)); + } + + /** + * Loads a preload. + * @param uri URI to load from + * @param originalUri URI to show in an error message if the preload is invalid. + */ + public load(uri: string, originalUri: string) { + const promise = Promise.all([ + runPreload(uri, originalUri), + this.waitForAllCurrent(), + ]).then(([module]) => module.activate()); + + this.preloads.set(uri, promise); + return promise; + } + + /** + * Returns a promise that waits for all currently-registered preloads to + * activate before resolving. + */ + private waitForAllCurrent() { + return Promise.all([...this.preloads.values()].map(p => p.catch(err => err))); + } + }; + + const outputs = new class { + private outputs = new Map }>(); + /** + * Pushes the action onto the list of actions for the given output ID, + * ensuring that it's run in-order. + */ + public enqueue(outputId: string, action: (record: { cancelled: boolean }) => unknown) { + const record = this.outputs.get(outputId); + if (!record) { + this.outputs.set(outputId, { cancelled: false, queue: new Promise(r => r(action({ cancelled: false }))) }); + } else { + record.queue = record.queue.then(r => !record.cancelled && action(record)); + } + } + + /** + * Cancells the rendering of all outputs. + */ + public cancelAll() { + for (const record of this.outputs.values()) { + record.cancelled = true; + } + this.outputs.clear(); + } + + /** + * Cancels any ongoing rendering out an output. + */ + public cancelOutput(outputId: string) { + const output = this.outputs.get(outputId); + if (output) { + output.cancelled = true; + this.outputs.delete(outputId); + } + } + }; + + const renderers = new class { + private readonly _renderers = new Map(); constructor() { for (const renderer of rendererData) { - let loadPromise: Promise | undefined; - - const entry = { - load: () => { - if (!loadPromise) { - loadPromise = __import(renderer.entrypoint).then(module => { - return module.activate({ dependencies: renderer.dependencies }); - }); - } - return loadPromise; - }, - renderer: undefined, - }; - - for (const mime of renderer.mimeTypes || []) { - if (!this.mimeTypesToRenderers.has(mime)) { - this.mimeTypesToRenderers.set(mime, entry); + this._renderers.set(renderer.id, new Renderer(renderer, async (extensionId) => { + const ext = this._renderers.get(extensionId); + if (!ext) { + throw new Error(`Could not find extending renderer: ${extensionId}`); } - } + + await ext.load(); + })); } } - async renderMarkdown(element: HTMLElement, content: string): Promise { - const entry = this.mimeTypesToRenderers.get('text/markdown'); - if (!entry) { + public getRenderer(id: string): RendererApi | undefined { + return this._renderers.get(id)?.api; + } + + public async load(id: string) { + const renderer = this._renderers.get(id); + if (!renderer) { throw new Error('Could not find renderer'); } - const renderer = await entry.load(); - renderer.renderMarkup({ element, content }); + + return renderer.load(); + } + + + public clearAll() { + outputs.cancelAll(); + for (const renderer of this._renderers.values()) { + renderer.api?.destroyCell?.(); + } + } + + public clearOutput(rendererId: string, outputId: string) { + outputs.cancelOutput(outputId); + this._renderers.get(rendererId)?.api?.destroyCell?.(outputId); + } + + public async renderCustom(rendererId: string, outputId: string, info: ICreateCellInfo) { + const api = await this.load(rendererId); + if (!api) { + throw new Error(`renderer ${rendererId} did not return an API`); + } + + api.renderCell(outputId, info); + } + + public async renderMarkdown(id: string, element: HTMLElement, content: string): Promise { + const markdownRenderers = Array.from(this._renderers.values()) + .filter(renderer => renderer.data.mimeTypes.includes('text/markdown') && !renderer.data.extends); + + if (!markdownRenderers.length) { + throw new Error('Could not find renderer'); + } + + await Promise.all(markdownRenderers.map(x => x.load())); + + markdownRenderers[0].api?.renderCell(id, { + element, + value: content, + mime: 'text/markdown', + metadata: undefined, + outputId: undefined, + }); } }(); - vscode.postMessage({ __vscode_notebook_message: true, type: 'initialized' @@ -978,7 +1107,7 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Webv previewNode.innerText = ''; } else { previewContainerNode.classList.remove('emptyMarkdownCell'); - await markupRenderers.renderMarkdown(previewNode, content); + await renderers.renderMarkdown(cellId, previewNode, content); if (!hasPostedRenderedMathTelemetry) { const hasRenderedMath = previewNode.querySelector('.katex'); @@ -1025,7 +1154,7 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Webv cellId: drag.cellId, ctrlKey: e.ctrlKey, altKey: e.altKey, - position: { clientY: e.clientY }, + dragOffsetY: e.clientY, }); }); } @@ -1041,7 +1170,7 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Webv postNotebookMessage('cell-drag-start', { cellId: cellId, - position: { clientY: e.clientY }, + dragOffsetY: e.clientY, }); // Continuously send updates while dragging instead of relying on `updateDrag`. @@ -1053,7 +1182,7 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Webv postNotebookMessage('cell-drag', { cellId: cellId, - position: { clientY: this.currentDrag.clientY }, + dragOffsetY: this.currentDrag.clientY, }); requestAnimationFrame(trySendDragUpdate); }; @@ -1077,13 +1206,14 @@ async function webviewPreloads(style: PreloadStyles, rendererData: readonly Webv }(); } -export interface WebviewPreloadRenderer { +export interface RendererMetadata { + readonly id: string; readonly entrypoint: string; readonly mimeTypes: readonly string[]; - readonly dependencies: ReadonlyArray<{ entrypoint: string }>; + readonly extends: string | undefined; } -export function preloadsScriptStr(styleValues: PreloadStyles, renderers: readonly WebviewPreloadRenderer[]) { +export function preloadsScriptStr(styleValues: PreloadStyles, renderers: readonly RendererMetadata[]) { // TS will try compiling `import()` in webviePreloads, so use an helper function instead // of using `import(...)` directly return ` diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts index 57ed3710937..c1ebfdb4c06 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts @@ -147,7 +147,7 @@ export abstract class BaseCellViewModel extends Disposable { })); this._register(this._viewContext.notebookOptions.onDidChangeOptions(e => { - if (e.cellStatusBarVisibility) { + if (e.cellStatusBarVisibility || e.insertToolbarPosition) { this.layoutChange({}); } })); diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts index c757fcc2671..479bb337226 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts @@ -179,8 +179,8 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod + editorHeight + statusbarHeight; const outputShowMoreContainerOffset = totalHeight - - notebookLayoutConfiguration.bottomCellToolbarGap - - notebookLayoutConfiguration.bottomCellToolbarHeight / 2 + - notebookLayoutConfiguration.bottomToolbarGap + - notebookLayoutConfiguration.bottomToolbarHeight / 2 - outputShowMoreContainerHeight; const bottomToolbarOffset = this.viewContext.notebookOptions.computeBottomToolbarOffset(totalHeight); const editorWidth = state.outerWidth !== undefined @@ -209,11 +209,11 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod notebookLayoutConfiguration.cellTopMargin + notebookLayoutConfiguration.collapsedIndicatorHeight + notebookLayoutConfiguration.cellBottomMargin //CELL_BOTTOM_MARGIN - + notebookLayoutConfiguration.bottomCellToolbarGap //BOTTOM_CELL_TOOLBAR_GAP + + notebookLayoutConfiguration.bottomToolbarGap //BOTTOM_CELL_TOOLBAR_GAP + outputTotalHeight + outputShowMoreContainerHeight; const outputShowMoreContainerOffset = totalHeight - - notebookLayoutConfiguration.bottomCellToolbarGap - - notebookLayoutConfiguration.bottomCellToolbarHeight / 2 + - notebookLayoutConfiguration.bottomToolbarGap + - notebookLayoutConfiguration.bottomToolbarHeight / 2 - outputShowMoreContainerHeight; const bottomToolbarOffset = this.viewContext.notebookOptions.computeBottomToolbarOffset(totalHeight); const editorWidth = state.outerWidth !== undefined @@ -314,7 +314,7 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod + this.getEditorStatusbarHeight() + outputsTotalHeight + outputShowMoreContainerHeight - + layoutConfiguration.bottomCellToolbarGap //BOTTOM_CELL_TOOLBAR_GAP + + layoutConfiguration.bottomToolbarGap //BOTTOM_CELL_TOOLBAR_GAP + layoutConfiguration.cellBottomMargin; // CELL_BOTTOM_MARGIN; } diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/markdownCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/markdownCellViewModel.ts index 1703faa5359..c0b94acf079 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/markdownCellViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/markdownCellViewModel.ts @@ -29,7 +29,7 @@ export class MarkdownCellViewModel extends BaseCellViewModel implements ICellVie set renderedMarkdownHeight(newHeight: number) { if (this.getEditState() === CellEditState.Preview) { - const newTotalHeight = newHeight + this.viewContext.notebookOptions.getLayoutConfiguration().bottomCellToolbarGap; // BOTTOM_CELL_TOOLBAR_GAP; + const newTotalHeight = newHeight + this.viewContext.notebookOptions.getLayoutConfiguration().bottomToolbarGap; // BOTTOM_CELL_TOOLBAR_GAP; this.totalHeight = newTotalHeight; } } @@ -52,7 +52,7 @@ export class MarkdownCellViewModel extends BaseCellViewModel implements ICellVie this.totalHeight = this._editorHeight + layoutConfiguration.markdownCellTopMargin // MARKDOWN_CELL_TOP_MARGIN + layoutConfiguration.markdownCellBottomMargin // MARKDOWN_CELL_BOTTOM_MARGIN - + layoutConfiguration.bottomCellToolbarGap // BOTTOM_CELL_TOOLBAR_GAP + + layoutConfiguration.bottomToolbarGap // BOTTOM_CELL_TOOLBAR_GAP + this.viewContext.notebookOptions.computeStatusBarHeight(); } @@ -120,7 +120,7 @@ export class MarkdownCellViewModel extends BaseCellViewModel implements ICellVie editorWidth: initialNotebookLayoutInfo?.width ? this.viewContext.notebookOptions.computeMarkdownCellEditorWidth(initialNotebookLayoutInfo.width) : 0, - bottomToolbarOffset: this.viewContext.notebookOptions.getLayoutConfiguration().bottomCellToolbarGap, // BOTTOM_CELL_TOOLBAR_GAP, + bottomToolbarOffset: this.viewContext.notebookOptions.getLayoutConfiguration().bottomToolbarGap, // BOTTOM_CELL_TOOLBAR_GAP, totalHeight: 0 }; diff --git a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts index 816f3dbaf20..d8fcf0068cb 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts @@ -53,6 +53,8 @@ export const ACCESSIBLE_NOTEBOOK_DISPLAY_ORDER = [ export const BUILTIN_RENDERER_ID = '_builtin'; export const RENDERER_NOT_AVAILABLE = '_notAvailable'; +export type NotebookRendererEntrypoint = string | { extends: string; path: string }; + export enum NotebookRunState { Running = 1, Idle = 2 @@ -132,6 +134,7 @@ export const enum NotebookRendererMatch { export interface INotebookRendererInfo { id: string; displayName: string; + extends?: string; entrypoint: URI; preloads: ReadonlyArray; extensionLocation: URI; @@ -894,6 +897,9 @@ export const ShowCellStatusBarKey = 'notebook.showCellStatusBar'; export const NotebookTextDiffEditorPreview = 'notebook.diff.enablePreview'; export const ExperimentalUseMarkdownRenderer = 'notebook.experimental.useMarkdownRenderer'; export const ExperimentalCompactView = 'notebook.experimental.compactView'; +export const ExperimentalFocusIndicator = 'notebook.experimental.cellFocusIndicator'; +export const ExperimentalInsertToolbarPosition = 'notebook.experimental.insertToolbarPosition'; +export const ExperimentalGlobalToolbar = 'notebook.experimental.globalToolbar'; export const ExperimentalUndoRedoPerCell = 'notebook.experimental.undoRedoPerCell'; export const enum CellStatusbarAlignment { diff --git a/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts b/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts index 03cb38b4b63..5fc06a8afdb 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts @@ -28,8 +28,9 @@ import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; import { canceled } from 'vs/base/common/errors'; import { NotebookEditorInput } from 'vs/workbench/contrib/notebook/common/notebookEditorInput'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IFileWorkingCopyManager } from 'vs/workbench/services/workingCopy/common/fileWorkingCopyManager'; import { filter } from 'vs/base/common/objects'; +import { IFileWorkingCopyManager2 } from 'vs/workbench/services/workingCopy/common/fileWorkingCopyManager2'; +import { IResolvedUntitledFileWorkingCopy, IUntitledFileWorkingCopyModel, IUntitledFileWorkingCopyModelContentChangedEvent } from 'vs/workbench/services/workingCopy/common/untitledFileWorkingCopy'; //#region --- complex content provider @@ -425,13 +426,13 @@ export class SimpleNotebookEditorModel extends EditorModel implements INotebookE readonly onDidChangeOrphaned: Event = this._onDidChangeOrphaned.event; readonly onDidChangeReadonly: Event = this._onDidChangeReadonly.event; - private _workingCopy?: IResolvedFileWorkingCopy; + private _workingCopy?: IResolvedUntitledFileWorkingCopy | IResolvedFileWorkingCopy; private readonly _workingCopyListeners = new DisposableStore(); constructor( readonly resource: URI, readonly viewType: string, - private readonly _workingCopyManager: IFileWorkingCopyManager, + private readonly _workingCopyManager: IFileWorkingCopyManager2, @IInstantiationService private readonly _instantiationService: IInstantiationService, @IFileService private readonly _fileService: IFileService ) { @@ -461,11 +462,17 @@ export class SimpleNotebookEditorModel extends EditorModel implements INotebookE } isOrphaned(): boolean { - return this._workingCopy?.hasState(FileWorkingCopyState.ORPHAN) ?? false; + return !!this._workingCopy && SimpleNotebookEditorModel._isFileWorkingCopy(this._workingCopy) && this._workingCopy.hasState(FileWorkingCopyState.ORPHAN); } isReadonly(): boolean { - return this._workingCopy?.isReadonly() || this._fileService.hasCapability(this.resource, FileSystemProviderCapabilities.Readonly); + if (this._fileService.hasCapability(this.resource, FileSystemProviderCapabilities.Readonly)) { + return true; + } else if (this._workingCopy && SimpleNotebookEditorModel._isFileWorkingCopy(this._workingCopy)) { + return this._workingCopy?.isReadonly(); + } else { + return false; + } } revert(options?: IRevertOptions): Promise { @@ -479,14 +486,26 @@ export class SimpleNotebookEditorModel extends EditorModel implements INotebookE } async load(options?: INotebookLoadOptions): Promise { - const workingCopy = await this._workingCopyManager.resolve(this.resource, { reload: { async: !options?.forceReadFromFile } }); + if (!this._workingCopy) { - this._workingCopy = >workingCopy; - this._workingCopy.onDidChangeDirty(() => this._onDidChangeDirty.fire(), this._workingCopyListeners); - this._workingCopy.onDidSave(() => this._onDidSave.fire(), this._workingCopyListeners); - this._workingCopy.onDidChangeOrphaned(() => this._onDidChangeOrphaned.fire(), this._workingCopyListeners); - this._workingCopy.onDidChangeReadonly(() => this._onDidChangeReadonly.fire(), this._workingCopyListeners); + if (this.resource.scheme === Schemas.untitled) { + const workingCopy = await this._workingCopyManager.resolve({ untitledResource: this.resource }); + this._workingCopy = >workingCopy; + } else { + const workingCopy = await this._workingCopyManager.resolve(this.resource, { forceReadFromFile: options?.forceReadFromFile }); + this._workingCopyListeners.add(workingCopy.onDidSave(() => this._onDidSave.fire())); + this._workingCopyListeners.add(workingCopy.onDidChangeOrphaned(() => this._onDidChangeOrphaned.fire())); + this._workingCopyListeners.add(workingCopy.onDidChangeReadonly(() => this._onDidChangeReadonly.fire())); + this._workingCopy = >workingCopy; + } + this._workingCopy.onDidChangeDirty(() => this._onDidChangeDirty.fire(), undefined, this._workingCopyListeners); + + this._workingCopyListeners.add(this._workingCopy.onWillDispose(() => { + this._workingCopyListeners.clear(); + this._workingCopy?.model.dispose(); + })); } + assertType(this.isResolved()); return this; } @@ -501,11 +520,15 @@ export class SimpleNotebookEditorModel extends EditorModel implements INotebookE // the newly created editor input will pick it up and claim ownership of it. return this._instantiationService.createInstance(NotebookEditorInput, newWorkingCopy.resource, this.viewType, {}); } + + private static _isFileWorkingCopy(candidate: IResolvedUntitledFileWorkingCopy | IResolvedFileWorkingCopy): candidate is IResolvedFileWorkingCopy { + return typeof (>candidate).hasState === 'function'; + } } -export class NotebookFileWorkingCopyModel implements IFileWorkingCopyModel { +export class NotebookFileWorkingCopyModel implements IFileWorkingCopyModel, IUntitledFileWorkingCopyModel { - private readonly _onDidChangeContent = new Emitter(); + private readonly _onDidChangeContent = new Emitter(); private readonly _changeListener: IDisposable; readonly onDidChangeContent = this._onDidChangeContent.event; @@ -525,10 +548,10 @@ export class NotebookFileWorkingCopyModel implements IFileWorkingCopyModel { if (rawEvent.transient) { continue; } - //todo@jrieken,@rebornix forward this information from notebook model this._onDidChangeContent.fire({ - isRedoing: false, - isUndoing: false + isRedoing: false, //todo@rebornix forward this information from notebook model + isUndoing: false, + isEmpty: false, //_notebookModel.cells.length === 0 // todo@jrieken non transient metadata? }); break; } @@ -585,7 +608,9 @@ export class NotebookFileWorkingCopyModel implements IFileWorkingCopyModel { this._notebookModel.reset(data.cells, data.metadata, this._notebookSerializer.options); } - get versionId() { return this._notebookModel.alternativeVersionId; } + get versionId() { + return this._notebookModel.alternativeVersionId; + } pushStackElement(): void { this._notebookModel.pushStackElement('save', undefined, undefined); @@ -606,7 +631,8 @@ export class NotebookFileWorkingCopyModelFactory implements IFileWorkingCopyMode throw new Error('CANNOT open file notebook with this provider'); } - const data = await info.serializer.dataToNotebook(await streamToBuffer(stream)); + const bytes = await streamToBuffer(stream); + const data = await info.serializer.dataToNotebook(bytes); if (token.isCancellationRequested) { throw canceled(); diff --git a/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverServiceImpl.ts b/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverServiceImpl.ts index b4dcb5c68c9..75f9ce0c647 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverServiceImpl.ts @@ -11,16 +11,16 @@ import { combinedDisposable, DisposableStore, dispose, IDisposable, IReference, import { ComplexNotebookProviderInfo, INotebookService, SimpleNotebookProviderInfo } from 'vs/workbench/contrib/notebook/common/notebookService'; import { ILogService } from 'vs/platform/log/common/log'; import { Emitter, Event } from 'vs/base/common/event'; -import { FileWorkingCopyManager, IFileWorkingCopyManager } from 'vs/workbench/services/workingCopy/common/fileWorkingCopyManager'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; import { INotebookEditorModelResolverService } from 'vs/workbench/contrib/notebook/common/notebookEditorModelResolverService'; import { ResourceMap } from 'vs/base/common/map'; +import { FileWorkingCopyManager2, IFileWorkingCopyManager2 } from 'vs/workbench/services/workingCopy/common/fileWorkingCopyManager2'; class NotebookModelReferenceCollection extends ReferenceCollection> { private readonly _disposables = new DisposableStore(); - private readonly _workingCopyManagers = new Map>(); + private readonly _workingCopyManagers = new Map>(); private readonly _modelListener = new Map(); private readonly _onDidSaveNotebook = new Emitter(); @@ -70,10 +70,12 @@ class NotebookModelReferenceCollection extends ReferenceCollection>this._instantiationService.createInstance( - FileWorkingCopyManager, + const factory = new NotebookFileWorkingCopyModelFactory(viewType, this._notebookService); + workingCopyManager = >this._instantiationService.createInstance( + FileWorkingCopyManager2, workingCopyTypeId, - new NotebookFileWorkingCopyModelFactory(viewType, this._notebookService) + factory, + factory, ); this._workingCopyManagers.set(workingCopyTypeId, workingCopyManager); } diff --git a/src/vs/workbench/contrib/notebook/common/notebookOptions.ts b/src/vs/workbench/contrib/notebook/common/notebookOptions.ts index bd4cd916a29..2dde37f10d1 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookOptions.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookOptions.ts @@ -6,7 +6,9 @@ import { Emitter } from 'vs/base/common/event'; import { IDisposable } from 'vs/base/common/lifecycle'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { CellToolbarLocKey, CellToolbarVisibility, ExperimentalCompactView, ShowCellStatusBarKey } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellToolbarLocKey, CellToolbarVisibility, ExperimentalCompactView, ExperimentalFocusIndicator, ExperimentalGlobalToolbar, ExperimentalInsertToolbarPosition, ShowCellStatusBarKey } from 'vs/workbench/contrib/notebook/common/notebookCommon'; + +const SCROLLABLE_ELEMENT_PADDING_TOP = 18; let EDITOR_TOP_PADDING = 12; const editorTopPaddingChangeEmitter = new Emitter(); @@ -33,8 +35,8 @@ export interface NotebookLayoutConfiguration { markdownCellTopMargin: number; markdownCellBottomMargin: number; markdownPreviewPadding: number; - bottomCellToolbarGap: number; - bottomCellToolbarHeight: number; + bottomToolbarGap: number; + bottomToolbarHeight: number; editorToolbarHeight: number; editorTopPadding: number; editorBottomPadding: number; @@ -45,6 +47,9 @@ export interface NotebookLayoutConfiguration { cellToolbarLocation: string | { [key: string]: string }; cellToolbarInteraction: string; compactView: boolean; + focusIndicator: 'border' | 'gutter'; + insertToolbarPosition: 'betweenCells' | 'notebookToolbar' | 'both' | 'hidden'; + globalToolbar: boolean; } interface NotebookOptionsChangeEvent { @@ -53,6 +58,9 @@ interface NotebookOptionsChangeEvent { cellToolbarInteraction?: boolean; editorTopPadding?: boolean; compactView?: boolean; + focusIndicator?: boolean; + insertToolbarPosition?: boolean; + globalToolbar?: boolean; } const defaultConfigConstants = { @@ -61,7 +69,6 @@ const defaultConfigConstants = { markdownCellTopMargin: 8, markdownCellBottomMargin: 8, markdownCellLeftMargin: 32, - bottomCellToolbarGap: 18, }; const compactConfigConstants = { @@ -70,7 +77,6 @@ const compactConfigConstants = { markdownCellTopMargin: 6, markdownCellBottomMargin: 6, markdownCellLeftMargin: 32, - bottomCellToolbarGap: 12, }; export class NotebookOptions { @@ -81,9 +87,13 @@ export class NotebookOptions { constructor(readonly configurationService: IConfigurationService) { const showCellStatusBar = this.configurationService.getValue(ShowCellStatusBarKey); + const globalToolbar = this.configurationService.getValue(ExperimentalGlobalToolbar) ?? false; const cellToolbarLocation = this.configurationService.getValue(CellToolbarLocKey); const cellToolbarInteraction = this.configurationService.getValue(CellToolbarVisibility); const compactView = this.configurationService.getValue(ExperimentalCompactView); + const focusIndicator = this.configurationService.getValue<'border' | 'gutter'>(ExperimentalFocusIndicator) ?? 'border'; + const insertToolbarPosition = this.configurationService.getValue<'betweenCells' | 'notebookToolbar' | 'both' | 'hidden'>(ExperimentalInsertToolbarPosition) ?? 'both'; + const { bottomToolbarGap, bottomToolbarHeight } = this._computeBottomToolbarDimensions(compactView, insertToolbarPosition); this._disposables = []; this._layoutConfiguration = { @@ -94,16 +104,20 @@ export class NotebookOptions { cellStatusBarHeight: 22, cellOutputPadding: 14, markdownPreviewPadding: 8, - bottomCellToolbarHeight: 22, + bottomToolbarHeight: bottomToolbarHeight, + bottomToolbarGap: bottomToolbarGap, editorToolbarHeight: 0, editorTopPadding: EDITOR_TOP_PADDING, editorBottomPadding: 4, editorBottomPaddingWithoutStatusBar: 12, collapsedIndicatorHeight: 24, showCellStatusBar, + globalToolbar, cellToolbarLocation, cellToolbarInteraction, - compactView + compactView, + focusIndicator, + insertToolbarPosition }; this._disposables.push(this.configurationService.onDidChangeConfiguration(e => { @@ -111,8 +125,11 @@ export class NotebookOptions { let cellToolbarLocation = e.affectsConfiguration(CellToolbarLocKey); let cellToolbarInteraction = e.affectsConfiguration(CellToolbarVisibility); let compactView = e.affectsConfiguration(ExperimentalCompactView); + let focusIndicator = e.affectsConfiguration(ExperimentalFocusIndicator); + let insertToolbarPosition = e.affectsConfiguration(ExperimentalInsertToolbarPosition); + let globalToolbar = e.affectsConfiguration(ExperimentalGlobalToolbar); - if (!cellStatusBarVisibility && !cellToolbarLocation && !cellToolbarInteraction && !compactView) { + if (!cellStatusBarVisibility && !cellToolbarLocation && !cellToolbarInteraction && !compactView && !focusIndicator && !insertToolbarPosition && !globalToolbar) { return; } @@ -130,6 +147,10 @@ export class NotebookOptions { configuration.cellToolbarInteraction = this.configurationService.getValue(CellToolbarVisibility); } + if (focusIndicator) { + configuration.focusIndicator = this.configurationService.getValue<'border' | 'gutter'>(ExperimentalFocusIndicator) ?? 'border'; + } + if (compactView) { const compactViewValue = this.configurationService.getValue('notebook.experimental.compactView'); configuration = Object.assign(configuration, { @@ -138,6 +159,17 @@ export class NotebookOptions { configuration.compactView = compactViewValue; } + if (insertToolbarPosition) { + configuration.insertToolbarPosition = this.configurationService.getValue<'betweenCells' | 'notebookToolbar' | 'both' | 'hidden'>(ExperimentalInsertToolbarPosition) ?? 'both'; + const { bottomToolbarGap, bottomToolbarHeight } = this._computeBottomToolbarDimensions(configuration.compactView, configuration.insertToolbarPosition); + configuration.bottomToolbarHeight = bottomToolbarHeight; + configuration.bottomToolbarGap = bottomToolbarGap; + } + + if (globalToolbar) { + configuration.globalToolbar = this.configurationService.getValue(ExperimentalGlobalToolbar) ?? false; + } + this._layoutConfiguration = configuration; // trigger event @@ -145,7 +177,10 @@ export class NotebookOptions { cellStatusBarVisibility: cellStatusBarVisibility, cellToolbarLocation: cellToolbarLocation, cellToolbarInteraction: cellToolbarInteraction, - compactView: compactView + compactView: compactView, + focusIndicator: focusIndicator, + insertToolbarPosition: insertToolbarPosition, + globalToolbar: globalToolbar }); })); @@ -157,6 +192,23 @@ export class NotebookOptions { })); } + private _computeBottomToolbarDimensions(compactView: boolean, insertToolbarPosition: 'betweenCells' | 'notebookToolbar' | 'both' | 'hidden'): { bottomToolbarGap: number, bottomToolbarHeight: number } { + if (insertToolbarPosition === 'betweenCells' || insertToolbarPosition === 'both') { + return compactView ? { + bottomToolbarGap: 12, + bottomToolbarHeight: 22 + } : { + bottomToolbarGap: 18, + bottomToolbarHeight: 22 + }; + } else { + return { + bottomToolbarGap: 0, + bottomToolbarHeight: 0 + }; + } + } + getLayoutConfiguration(): NotebookLayoutConfiguration { return this._layoutConfiguration; } @@ -164,14 +216,14 @@ export class NotebookOptions { computeCollapsedMarkdownCellHeight(): number { return this._layoutConfiguration.markdownCellTopMargin + this._layoutConfiguration.collapsedIndicatorHeight - + this._layoutConfiguration.bottomCellToolbarGap + + this._layoutConfiguration.bottomToolbarGap + this._layoutConfiguration.markdownCellBottomMargin; } computeBottomToolbarOffset(totalHeight: number) { return totalHeight - - this._layoutConfiguration.bottomCellToolbarGap - - this._layoutConfiguration.bottomCellToolbarHeight / 2; + - this._layoutConfiguration.bottomToolbarGap + - this._layoutConfiguration.bottomToolbarHeight / 2; } computeCodeCellEditorWidth(outerWidth: number): number { @@ -265,11 +317,19 @@ export class NotebookOptions { computeIndicatorPosition(totalHeight: number) { return { - bottomIndicatorTop: totalHeight - this._layoutConfiguration.bottomCellToolbarGap - this._layoutConfiguration.cellBottomMargin, - verticalIndicatorHeight: totalHeight - this._layoutConfiguration.bottomCellToolbarGap + bottomIndicatorTop: totalHeight - this._layoutConfiguration.bottomToolbarGap - this._layoutConfiguration.cellBottomMargin, + verticalIndicatorHeight: totalHeight - this._layoutConfiguration.bottomToolbarGap }; } + computeTopInserToolbarHeight(): number { + if (this._layoutConfiguration.insertToolbarPosition === 'betweenCells' || this._layoutConfiguration.insertToolbarPosition === 'both') { + return SCROLLABLE_ELEMENT_PADDING_TOP; + } else { + return 0; + } + } + dispose() { this._disposables.forEach(d => d.dispose()); this._disposables = []; diff --git a/src/vs/workbench/contrib/notebook/common/notebookOutputRenderer.ts b/src/vs/workbench/contrib/notebook/common/notebookOutputRenderer.ts index b698cd5b3ee..5025db38aeb 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookOutputRenderer.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookOutputRenderer.ts @@ -8,7 +8,7 @@ import { Iterable } from 'vs/base/common/iterator'; import { joinPath } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; -import { INotebookRendererInfo, NotebookRendererMatch } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { INotebookRendererInfo, NotebookRendererEntrypoint, NotebookRendererMatch } from 'vs/workbench/contrib/notebook/common/notebookCommon'; class DependencyList { private readonly value: ReadonlySet; @@ -34,6 +34,7 @@ class DependencyList { export class NotebookOutputRendererInfo implements INotebookRendererInfo { readonly id: string; + readonly extends?: string; readonly entrypoint: URI; readonly displayName: string; readonly extensionLocation: URI; @@ -49,7 +50,7 @@ export class NotebookOutputRendererInfo implements INotebookRendererInfo { constructor(descriptor: { readonly id: string; readonly displayName: string; - readonly entrypoint: string; + readonly entrypoint: NotebookRendererEntrypoint; readonly mimeTypes: readonly string[]; readonly extension: IExtensionDescription; readonly dependencies: readonly string[] | undefined; @@ -58,7 +59,14 @@ export class NotebookOutputRendererInfo implements INotebookRendererInfo { this.id = descriptor.id; this.extensionId = descriptor.extension.identifier; this.extensionLocation = descriptor.extension.extensionLocation; - this.entrypoint = joinPath(this.extensionLocation, descriptor.entrypoint); + + if (typeof descriptor.entrypoint === 'string') { + this.entrypoint = joinPath(this.extensionLocation, descriptor.entrypoint); + } else { + this.extends = descriptor.entrypoint.extends; + this.entrypoint = joinPath(this.extensionLocation, descriptor.entrypoint.path); + } + this.displayName = descriptor.displayName; this.mimeTypes = descriptor.mimeTypes; this.mimeTypeGlobs = this.mimeTypes.map(pattern => glob.parse(pattern)); @@ -103,6 +111,10 @@ export class NotebookOutputRendererInfo implements INotebookRendererInfo { } private matchesMimeTypeOnly(mimeType: string) { + if (this.extends !== undefined) { + return false; + } + return this.mimeTypeGlobs.some(pattern => pattern(mimeType)) || this.mimeTypes.some(pattern => pattern === mimeType); } } diff --git a/src/vs/workbench/contrib/notebook/test/notebookCellList.test.ts b/src/vs/workbench/contrib/notebook/test/notebookCellList.test.ts index 84e2e5aad25..122ffb94bc9 100644 --- a/src/vs/workbench/contrib/notebook/test/notebookCellList.test.ts +++ b/src/vs/workbench/contrib/notebook/test/notebookCellList.test.ts @@ -4,12 +4,15 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { SCROLLABLE_ELEMENT_PADDING_TOP } from 'vs/workbench/contrib/notebook/browser/constants'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { NotebookOptions } from 'vs/workbench/contrib/notebook/common/notebookOptions'; import { createNotebookCellList, setupInstantiationService, withTestNotebook } from 'vs/workbench/contrib/notebook/test/testNotebookEditor'; suite('NotebookCellList', () => { const instantiationService = setupInstantiationService(); + const notebookDefaultOptions = new NotebookOptions(instantiationService.get(IConfigurationService)); + const topInsertToolbarHeight = notebookDefaultOptions.computeTopInserToolbarHeight(); test('revealElementsInView: reveal fully visible cell should not scroll', async function () { await withTestNotebook( @@ -32,7 +35,7 @@ suite('NotebookCellList', () => { cellList.attachViewModel(viewModel); // render height 210, it can render 3 full cells and 1 partial cell - cellList.layout(210 + SCROLLABLE_ELEMENT_PADDING_TOP, 100); + cellList.layout(210 + topInsertToolbarHeight, 100); // scroll a bit, scrollTop to bottom: 5, 215 cellList.scrollTop = 5; @@ -77,7 +80,7 @@ suite('NotebookCellList', () => { cellList.attachViewModel(viewModel); // render height 210, it can render 3 full cells and 1 partial cell - cellList.layout(210 + SCROLLABLE_ELEMENT_PADDING_TOP, 100); + cellList.layout(210 + topInsertToolbarHeight, 100); // init scrollTop and scrollBottom assert.deepStrictEqual(cellList.scrollTop, 0); @@ -116,12 +119,12 @@ suite('NotebookCellList', () => { }); const cellList = createNotebookCellList(instantiationService); - // without additionalscrollheight, the last 20 px will always be hidden due to `SCROLLABLE_ELEMENT_PADDING_TOP` + // without additionalscrollheight, the last 20 px will always be hidden due to `topInsertToolbarHeight` cellList.updateOptions({ additionalScrollHeight: 100 }); cellList.attachViewModel(viewModel); // render height 210, it can render 3 full cells and 1 partial cell - cellList.layout(210 + SCROLLABLE_ELEMENT_PADDING_TOP, 100); + cellList.layout(210 + topInsertToolbarHeight, 100); // init scrollTop and scrollBottom assert.deepStrictEqual(cellList.scrollTop, 0); @@ -154,7 +157,7 @@ suite('NotebookCellList', () => { cellList.attachViewModel(viewModel); // render height 210, it can render 3 full cells and 1 partial cell - cellList.layout(210 + SCROLLABLE_ELEMENT_PADDING_TOP, 100); + cellList.layout(210 + topInsertToolbarHeight, 100); // init scrollTop and scrollBottom assert.deepStrictEqual(cellList.scrollTop, 0); @@ -196,7 +199,7 @@ suite('NotebookCellList', () => { cellList.attachViewModel(viewModel); // render height 210, it can render 3 full cells and 1 partial cell - cellList.layout(210 + SCROLLABLE_ELEMENT_PADDING_TOP, 100); + cellList.layout(210 + topInsertToolbarHeight, 100); // init scrollTop and scrollBottom assert.deepStrictEqual(cellList.scrollTop, 0); @@ -249,7 +252,7 @@ suite('NotebookCellList', () => { cellList.attachViewModel(viewModel); // render height 210, it can render 3 full cells and 1 partial cell - cellList.layout(210 + SCROLLABLE_ELEMENT_PADDING_TOP, 100); + cellList.layout(210 + topInsertToolbarHeight, 100); // init scrollTop and scrollBottom assert.deepStrictEqual(cellList.scrollTop, 0); @@ -283,7 +286,7 @@ suite('NotebookCellList', () => { cellList.attachViewModel(viewModel); // render height 210, it can render 3 full cells and 1 partial cell - cellList.layout(210 + SCROLLABLE_ELEMENT_PADDING_TOP, 100); + cellList.layout(210 + topInsertToolbarHeight, 100); // init scrollTop and scrollBottom assert.deepStrictEqual(cellList.scrollTop, 0); diff --git a/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts b/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts index 04da8ba66b7..7f474ee7440 100644 --- a/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts +++ b/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts @@ -177,7 +177,7 @@ function _createTestNotebookEditor(instantiationService: TestInstantiationServic const viewContext = new ViewContext(new NotebookOptions(instantiationService.get(IConfigurationService)), new NotebookEventDispatcher()); const viewModel: NotebookViewModel = instantiationService.createInstance(NotebookViewModel, viewType, model.notebook, viewContext, null); - const cellList = createNotebookCellList(instantiationService); + const cellList = createNotebookCellList(instantiationService, viewContext); cellList.attachViewModel(viewModel); const listViewInfoAccessor = new ListViewInfoAccessor(cellList); @@ -275,7 +275,7 @@ export async function withTestNotebook(cells: [source: string, lang: st return res; } -export function createNotebookCellList(instantiationService: TestInstantiationService) { +export function createNotebookCellList(instantiationService: TestInstantiationService, viewContext?: ViewContext) { const delegate: IListVirtualDelegate = { getHeight(element: CellViewModel) { return element.getHeight(17); }, getTemplateId() { return 'template'; } @@ -293,6 +293,7 @@ export function createNotebookCellList(instantiationService: TestInstantiationSe 'NotebookCellList', DOM.$('container'), DOM.$('body'), + viewContext ?? new ViewContext(new NotebookOptions(instantiationService.get(IConfigurationService)), new NotebookEventDispatcher()), delegate, [renderer], instantiationService.get(IContextKeyService), diff --git a/src/vs/workbench/contrib/preferences/browser/preferencesRenderers.ts b/src/vs/workbench/contrib/preferences/browser/preferencesRenderers.ts index 06e6fba025e..be7329df4c1 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferencesRenderers.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferencesRenderers.ts @@ -822,7 +822,7 @@ class EditSettingRenderer extends Disposable { private onEditSettingClicked(editPreferenceWidget: EditPreferenceWidget, e: IEditorMouseEvent): void { EventHelper.stop(e.event, true); - const anchor = { x: e.event.posx, y: e.event.posy + 10 }; + const anchor = { x: e.event.posx, y: e.event.posy }; const actions = this.getSettings(editPreferenceWidget.getLine()).length === 1 ? this.getActions(editPreferenceWidget.preferences[0], this.getConfigurationsMap()[editPreferenceWidget.preferences[0].key]) : editPreferenceWidget.preferences.map(setting => new SubmenuAction(`preferences.submenu.${setting.key}`, setting.key, this.getActions(setting, this.getConfigurationsMap()[setting.key]))); this.contextMenuService.showContextMenu({ diff --git a/src/vs/workbench/contrib/webview/browser/pre/host.js b/src/vs/workbench/contrib/webview/browser/pre/host.js index 8310f417ca0..7e773adefab 100644 --- a/src/vs/workbench/contrib/webview/browser/pre/host.js +++ b/src/vs/workbench/contrib/webview/browser/pre/host.js @@ -6,8 +6,13 @@ import { createWebviewManager } from './main.js'; -const id = document.location.search.match(/\bid=([\w-]+)/)[1]; -const onElectron = /platform=electron/.test(document.location.search); +const searchParams = new URL(location.toString()).searchParams; +const id = searchParams.get('id'); +if (!id) { + throw new Error('Could not resolve webview id. Webview will not work.\nThis is usually caused by incorrectly trying to navigate in a webview'); +} + +const onElectron = searchParams.get('platform') === 'electron'; const hostMessaging = new class HostMessaging { constructor() { diff --git a/src/vs/workbench/contrib/webview/browser/pre/main.js b/src/vs/workbench/contrib/webview/browser/pre/main.js index a31f57f26ce..a5faa4d3316 100644 --- a/src/vs/workbench/contrib/webview/browser/pre/main.js +++ b/src/vs/workbench/contrib/webview/browser/pre/main.js @@ -4,10 +4,12 @@ *--------------------------------------------------------------------------------------------*/ // @ts-check +/// + /** * @typedef {{ * postMessage: (channel: string, data?: any) => void, - * onMessage: (channel: string, handler: any) => void, + * onMessage: (channel: string, handler: (event: MessageEvent, data: any) => void) => void, * focusIframeOnCreate?: boolean, * ready?: Promise, * onIframeLoaded?: (iframe: HTMLIFrameElement) => void, @@ -56,6 +58,18 @@ const getPendingFrame = () => { return /** @type {HTMLIFrameElement} */ (document.getElementById('pending-frame')); }; +/** + * @template T + * @param {T | undefined | null} obj + * @return {T} + */ +function assertIsDefined(obj) { + if (typeof obj === 'undefined' || obj === null) { + throw new Error('Found unexpected null'); + } + return obj; +} + const vscodePostMessageFuncName = '__vscode_post_message__'; const defaultStyles = document.createElement('style'); @@ -202,6 +216,9 @@ const workerReady = new Promise(async (resolve, reject) => { async registration => { await navigator.serviceWorker.ready; + /** + * @param {MessageEvent} event + */ const versionHandler = (event) => { if (event.data.channel !== 'version') { return; @@ -218,7 +235,7 @@ const workerReady = new Promise(async (resolve, reject) => { } }; navigator.serviceWorker.addEventListener('message', versionHandler); - registration.active.postMessage({ channel: 'version' }); + assertIsDefined(registration.active).postMessage({ channel: 'version' }); }, error => { reject(new Error(`Could not register service workers: ${error}.`)); @@ -231,6 +248,7 @@ const workerReady = new Promise(async (resolve, reject) => { export async function createWebviewManager(host) { // state let firstLoad = true; + /** @type {any} */ let loadTimeout; let styleVersion = 0; @@ -241,7 +259,7 @@ export async function createWebviewManager(host) { /** @type {number | undefined} */ initialScrollProgress: undefined, - /** @type {{ [key: string]: string }} */ + /** @type {{ [key: string]: string } | undefined} */ styles: undefined, /** @type {string | undefined} */ @@ -253,13 +271,13 @@ export async function createWebviewManager(host) { host.onMessage('did-load-resource', (_event, data) => { navigator.serviceWorker.ready.then(registration => { - registration.active.postMessage({ channel: 'did-load-resource', data }, data.data?.buffer ? [data.data.buffer] : []); + assertIsDefined(registration.active).postMessage({ channel: 'did-load-resource', data }, data.data?.buffer ? [data.data.buffer] : []); }); }); host.onMessage('did-load-localhost', (_event, data) => { navigator.serviceWorker.ready.then(registration => { - registration.active.postMessage({ channel: 'did-load-localhost', data }); + assertIsDefined(registration.active).postMessage({ channel: 'did-load-localhost', data }); }); }); @@ -282,7 +300,9 @@ export async function createWebviewManager(host) { if (body) { body.classList.remove('vscode-light', 'vscode-dark', 'vscode-high-contrast'); - body.classList.add(initData.activeTheme); + if (initData.activeTheme) { + body.classList.add(initData.activeTheme); + } body.dataset.vscodeThemeKind = initData.activeTheme; body.dataset.vscodeThemeName = initData.themeName || ''; @@ -435,6 +455,9 @@ export async function createWebviewManager(host) { let isHandlingScroll = false; + /** + * @param {WheelEvent} event + */ const handleWheel = (event) => { if (isHandlingScroll) { return; @@ -450,15 +473,21 @@ export async function createWebviewManager(host) { }); }; + /** + * @param {Event} event + */ const handleInnerScroll = (event) => { - if (!event.target || !event.target.body) { - return; - } if (isHandlingScroll) { return; } - const progress = event.currentTarget.scrollY / event.target.body.clientHeight; + const target = /** @type {HTMLDocument | null} */ (event.target); + const currentTarget = /** @type {Window | null} */ (event.currentTarget); + if (!target || !currentTarget || !target.body) { + return; + } + + const progress = currentTarget.scrollY / target.body.clientHeight; if (isNaN(progress)) { return; } @@ -475,6 +504,19 @@ export async function createWebviewManager(host) { }; /** + * @typedef {{ + * contents: string; + * options: { + * readonly allowScripts: boolean; + * readonly allowMultipleAPIAcquire: boolean; + * } + * state: any; + * resourceEndpoint: string; + * }} ContentUpdateData + */ + + /** + * @param {ContentUpdateData} data * @return {string} */ function toContentHtml(data) { @@ -484,7 +526,10 @@ export async function createWebviewManager(host) { newDocument.querySelectorAll('a').forEach(a => { if (!a.title) { - a.title = a.getAttribute('href'); + const href = a.getAttribute('href'); + if (typeof href === 'string') { + a.title = href; + } } }); @@ -509,8 +554,11 @@ export async function createWebviewManager(host) { try { // Attempt to rewrite CSPs that hardcode old-style resource endpoint const endpointUrl = new URL(data.resourceEndpoint); - const newCsp = csp.getAttribute('content').replace(/(vscode-webview-resource|vscode-resource):(?=(\s|;|$))/g, endpointUrl.origin); - csp.setAttribute('content', newCsp); + const cspContent = csp.getAttribute('content'); + if (cspContent) { + const newCsp = cspContent.replace(/(vscode-webview-resource|vscode-resource):(?=(\s|;|$))/g, endpointUrl.origin); + csp.setAttribute('content', newCsp); + } } catch (e) { console.error(`Could not rewrite csp: ${e}`); } @@ -563,7 +611,7 @@ export async function createWebviewManager(host) { // update iframe-contents let updateId = 0; - host.onMessage('content', async (_event, data) => { + host.onMessage('content', async (_event, /** @type {ContentUpdateData} */ data) => { const currentUpdateId = ++updateId; try { @@ -586,18 +634,19 @@ export async function createWebviewManager(host) { const frame = getActiveFrame(); const wasFirstLoad = firstLoad; // keep current scrollY around and use later + /** @type {(body: HTMLElement, window: Window) => void} */ let setInitialScrollPosition; if (firstLoad) { firstLoad = false; setInitialScrollPosition = (body, window) => { - if (!isNaN(initData.initialScrollProgress)) { + if (typeof initData.initialScrollProgress === 'number' && !isNaN(initData.initialScrollProgress)) { if (window.scrollY === 0) { window.scroll(0, body.clientHeight * initData.initialScrollProgress); } } }; } else { - const scrollY = frame && frame.contentDocument && frame.contentDocument.body ? frame.contentWindow.scrollY : 0; + const scrollY = frame && frame.contentDocument && frame.contentDocument.body ? assertIsDefined(frame.contentWindow).scrollY : 0; setInitialScrollPosition = (body, window) => { if (window.scrollY === 0) { window.scroll(0, scrollY); @@ -656,15 +705,16 @@ export async function createWebviewManager(host) { return; } - if (newFrame.contentDocument.readyState !== 'loading') { + const contentDocument = assertIsDefined(newFrame.contentDocument); + if (contentDocument.readyState !== 'loading') { clearInterval(interval); - onFrameLoaded(newFrame.contentDocument); + onFrameLoaded(contentDocument); } }, 10); } else { - newFrame.contentWindow.addEventListener('DOMContentLoaded', e => { + assertIsDefined(newFrame.contentWindow).addEventListener('DOMContentLoaded', e => { const contentDocument = e.target ? (/** @type {HTMLDocument} */ (e.target)) : undefined; - onFrameLoaded(contentDocument); + onFrameLoaded(assertIsDefined(contentDocument)); }); } @@ -692,7 +742,7 @@ export async function createWebviewManager(host) { newFrame.setAttribute('id', 'active-frame'); newFrame.style.visibility = 'visible'; if (host.focusIframeOnCreate) { - newFrame.contentWindow.focus(); + assertIsDefined(newFrame.contentWindow).focus(); } contentWindow.addEventListener('scroll', handleInnerScroll); @@ -720,10 +770,12 @@ export async function createWebviewManager(host) { loadTimeout = setTimeout(() => { clearTimeout(loadTimeout); loadTimeout = undefined; - onLoad(newFrame.contentDocument, newFrame.contentWindow); + onLoad(assertIsDefined(newFrame.contentDocument), assertIsDefined(newFrame.contentWindow)); }, 200); - newFrame.contentWindow.addEventListener('load', function (e) { + const contentWindow = assertIsDefined(newFrame.contentWindow); + + contentWindow.addEventListener('load', function (e) { const contentDocument = /** @type {Document} */ (e.target); if (loadTimeout) { @@ -734,11 +786,16 @@ export async function createWebviewManager(host) { }); // Bubble out various events - newFrame.contentWindow.addEventListener('click', handleInnerClick); - newFrame.contentWindow.addEventListener('auxclick', handleAuxClick); - newFrame.contentWindow.addEventListener('keydown', handleInnerKeydown); - newFrame.contentWindow.addEventListener('keyup', handleInnerUp); - newFrame.contentWindow.addEventListener('contextmenu', e => { + contentWindow.addEventListener('click', handleInnerClick); + contentWindow.addEventListener('auxclick', handleAuxClick); + contentWindow.addEventListener('keydown', handleInnerKeydown); + contentWindow.addEventListener('keyup', handleInnerUp); + contentWindow.addEventListener('contextmenu', e => { + if (e.defaultPrevented) { + // Extension code has already handled this event + return; + } + e.preventDefault(); host.postMessage('did-context-menu', { clientX: e.clientX, @@ -760,7 +817,7 @@ export async function createWebviewManager(host) { if (!pending) { const target = getActiveFrame(); if (target) { - target.contentWindow.postMessage(data.message, '*', data.transfer); + assertIsDefined(target.contentWindow).postMessage(data.message, '*', data.transfer); return; } } @@ -776,7 +833,7 @@ export async function createWebviewManager(host) { if (!target) { return; } - target.contentDocument.execCommand(data); + assertIsDefined(target.contentDocument).execCommand(data); }); trackFocus({ @@ -784,7 +841,7 @@ export async function createWebviewManager(host) { onBlur: () => host.postMessage('did-blur') }); - (/** @type {any} */ (window))[vscodePostMessageFuncName] = (command, data, transfer) => { + (/** @type {any} */ (window))[vscodePostMessageFuncName] = (/** @type {string} */ command, /** @type {any} */ data) => { switch (command) { case 'onmessage': case 'do-update-state': diff --git a/src/vs/workbench/contrib/webview/browser/pre/service-worker.js b/src/vs/workbench/contrib/webview/browser/pre/service-worker.js index e61fc59ad84..20576472cc5 100644 --- a/src/vs/workbench/contrib/webview/browser/pre/service-worker.js +++ b/src/vs/workbench/contrib/webview/browser/pre/service-worker.js @@ -66,9 +66,15 @@ class RequestStore { create() { const requestId = ++this.requestPool; + /** @type {undefined | ((x: T) => void)} */ let resolve; + + /** @type {Promise} */ const promise = new Promise(r => resolve = r); - const entry = { resolve, promise }; + + /** @type {RequestStoreEntry} */ + const entry = { resolve: /** @type {(x: T) => void} */ (resolve), promise }; + this.map.set(requestId, entry); const dispose = () => { @@ -156,7 +162,6 @@ sw.addEventListener('message', async (event) => { } case 'did-load-localhost': { - const webviewId = getWebviewIdForClient(event.source); const data = event.data.data; if (!localhostRequestStore.resolve(data.id, data.location)) { console.log('Could not resolve unknown localhost', data.origin); @@ -205,11 +210,16 @@ sw.addEventListener('activate', (event) => { async function processResourceRequest(event, requestUrl) { const client = await sw.clients.get(event.clientId); if (!client) { - console.log('Could not find inner client for request'); + console.error('Could not find inner client for request'); return notFound(); } const webviewId = getWebviewIdForClient(client); + if (!webviewId) { + console.error('Could not resolve webview id'); + return notFound(); + } + const resourcePath = requestUrl.pathname.startsWith(resourceRoot + '/') ? requestUrl.pathname.slice(resourceRoot.length) : requestUrl.pathname; /** @@ -229,18 +239,18 @@ async function processResourceRequest(event, requestUrl) { } } - const cacheHeaders = entry.etag ? { - 'ETag': entry.etag, - 'Cache-Control': 'no-cache' - } : {}; - + /** @type {Record} */ + const headers = { + 'Content-Type': entry.mime, + 'Access-Control-Allow-Origin': '*', + }; + if (entry.etag) { + headers['ETag'] = entry.etag; + headers['Cache-Control'] = 'no-cache'; + } const response = new Response(entry.body, { status: 200, - headers: { - 'Content-Type': entry.mime, - 'Access-Control-Allow-Origin': '*', - ...cacheHeaders - } + headers }); if (entry.etag) { @@ -273,23 +283,30 @@ async function processResourceRequest(event, requestUrl) { } /** - * @param {*} event + * @param {FetchEvent} event * @param {URL} requestUrl + * @return {Promise} */ async function processLocalhostRequest(event, requestUrl) { const client = await sw.clients.get(event.clientId); if (!client) { // This is expected when requesting resources on other localhost ports // that are not spawned by vs code - return undefined; + return fetch(event.request); } const webviewId = getWebviewIdForClient(client); + if (!webviewId) { + console.error('Could not resolve webview id'); + return fetch(event.request); + } + const origin = requestUrl.origin; /** - * @param {string} redirectOrigin + * @param {string | undefined} redirectOrigin + * @return {Promise} */ - const resolveRedirect = (redirectOrigin) => { + const resolveRedirect = async (redirectOrigin) => { if (!redirectOrigin) { return fetch(event.request); } @@ -318,16 +335,24 @@ async function processLocalhostRequest(event, requestUrl) { return promise.then(resolveRedirect); } +/** + * @param {Client} client + * @returns {string | null} + */ function getWebviewIdForClient(client) { const requesterClientUrl = new URL(client.url); - return requesterClientUrl.search.match(/\bid=([a-z0-9-]+)/i)[1]; + return requesterClientUrl.searchParams.get('id'); } +/** + * @param {string} webviewId + * @returns {Promise} + */ async function getOuterIframeClient(webviewId) { const allClients = await sw.clients.matchAll({ includeUncontrolled: true }); return allClients.find(client => { const clientUrl = new URL(client.url); const hasExpectedPathName = (clientUrl.pathname === `${rootPath}/` || clientUrl.pathname === `${rootPath}/index.html` || clientUrl.pathname === `${rootPath}/electron-browser-index.html`); - return hasExpectedPathName && clientUrl.search.match(new RegExp('\\bid=' + webviewId)); + return hasExpectedPathName && clientUrl.searchParams.get('id') === webviewId; }); } diff --git a/src/vs/workbench/contrib/workspace/browser/workspace.contribution.ts b/src/vs/workbench/contrib/workspace/browser/workspace.contribution.ts index 4abb2b498c6..0dbac3b6736 100644 --- a/src/vs/workbench/contrib/workspace/browser/workspace.contribution.ts +++ b/src/vs/workbench/contrib/workspace/browser/workspace.contribution.ts @@ -16,7 +16,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { IWorkspaceTrustManagementService, IWorkspaceTrustRequestService, workspaceTrustToString } from 'vs/platform/workspace/common/workspaceTrust'; import { Extensions as WorkbenchExtensions, IWorkbenchContribution, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; -import { Codicon } from 'vs/base/common/codicons'; +import { Codicon, registerCodicon } from 'vs/base/common/codicons'; import { ThemeColor } from 'vs/workbench/api/common/extHostTypes'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; @@ -47,7 +47,13 @@ import { getVirtualWorkspaceScheme } from 'vs/platform/remote/common/remoteHosts import { verifyMicrosoftInternalDomain } from 'vs/platform/telemetry/common/commonProperties'; const BANNER_RESTRICTED_MODE = 'workbench.banner.restrictedMode'; +const BANNER_VIRTUAL_WORKSPACE = 'workbench.banner.virtualWorkspace'; +const BANNER_VIRTUAL_AND_RESTRICTED = 'workbench.banner.virtualAndRestricted'; const STARTUP_PROMPT_SHOWN_KEY = 'workspace.trust.startupPrompt.shown'; +const BANNER_RESTRICTED_MODE_DISMISSED_KEY = 'workbench.banner.restrictedMode.dismissed'; +const BANNER_VIRTUAL_WORKSPACE_DISMISSED_KEY = 'workbench.banner.virtualWorkspace.dismissed'; + +const infoIcon = registerCodicon('workspace-banner-warning-icon', Codicon.info); /* * Trust Request UX Handler @@ -149,6 +155,7 @@ export class WorkspaceTrustRequestHandler extends Disposable implements IWorkben private showModalOnStart(): void { if (this.workspaceTrustManagementService.isWorkpaceTrusted()) { + this.updateWorkbenchIndicators(true); return; } @@ -198,13 +205,52 @@ export class WorkspaceTrustRequestHandler extends Disposable implements IWorkben this.statusbarService.updateEntryVisibility(this.entryId, false); } - private getBannerItem(): IBannerItem { - return { - id: BANNER_RESTRICTED_MODE, - icon: shieldIcon, - ariaLabel: localize('restrictedModeBannerAriaLabel', "Restricted Mode is intended for safe code browsing. Trust this folder to enable all features. Use navigation keys to access banner actions."), - message: localize('restrictedModeBannerMessage', "Restricted Mode is intended for safe code browsing. Trust this folder to enable all features."), - actions: [ + private getBannerItem(isVirtualWorkspace: boolean, restrictedMode: boolean): IBannerItem | undefined { + + const dismissedVirtual = this.storageService.getBoolean(BANNER_VIRTUAL_WORKSPACE_DISMISSED_KEY, StorageScope.WORKSPACE, false); + const dismissedRestricted = this.storageService.getBoolean(BANNER_RESTRICTED_MODE_DISMISSED_KEY, StorageScope.WORKSPACE, false); + + // all important info has been dismissed + if (dismissedVirtual && dismissedRestricted) { + return undefined; + } + + // don't show restricted mode only banner + if (dismissedRestricted && !isVirtualWorkspace) { + return undefined; + } + + // don't show virtual workspace only banner + if (dismissedVirtual && !restrictedMode) { + return undefined; + } + + const choose = (virtual: any, restricted: any, virtualAndRestricted: any) => { + return (isVirtualWorkspace && !dismissedVirtual) && (restrictedMode && !dismissedRestricted) ? virtualAndRestricted : ((isVirtualWorkspace && !dismissedVirtual) ? virtual : restricted); + }; + + const id = choose(BANNER_VIRTUAL_WORKSPACE, BANNER_RESTRICTED_MODE, BANNER_VIRTUAL_AND_RESTRICTED); + const icon = choose(infoIcon, shieldIcon, infoIcon); + const ariaLabel = choose( + localize('virtualBannerAriaLabel', "Some features are not available because the current workspace is backed by a virtual file system. Use navigation keys to access banner actions."), + localize('restrictedModeBannerAriaLabel', "Restricted Mode is intended for safe code browsing. Trust this folder to enable all features. Use navigation keys to access banner actions."), + localize('virtualAndRestrictedModeBannerAriaLabel', "Some features are not available because the current workspace is backed by a virtual file system and is not trusted. You can trust this workspace to enable some of these features. Use navigation keys to access banner actions."), + ); + + const message = choose( + localize('virtualBannerMessage', "Some features are not available because the current workspace is backed by a virtual file system."), + localize('restrictedModeBannerMessage', "Restricted Mode is intended for safe code browsing. Trust this folder to enable all features."), + localize('virtualAndRestrictedModeBannerMessage', "Some features are not available because the current workspace is backed by a virtual file system and is not trusted. You can trust this workspace to enabled some of these features."), + ); + + const actions = choose( + [ + { + label: localize('virtualBannerLearnMore', "Learn More"), + href: 'https://aka.ms/vscode-virtual-workspaces' + } + ], + [ { label: localize('restrictedModeBannerManage', "Manage"), href: 'command:workbench.trust.manage' @@ -214,7 +260,33 @@ export class WorkspaceTrustRequestHandler extends Disposable implements IWorkben href: 'https://aka.ms/vscode-workspace-trust' } ], - scope: StorageScope.WORKSPACE, + [ + { + label: localize('virtualAndRestrictedModeBannerManage', "Manage Trust"), + href: 'command:workbench.trust.manage' + }, + { + label: localize('virtualBannerLearnMore', "Learn More"), + href: 'https://aka.ms/vscode-virtual-workspaces' + } + ] + ); + + return { + id, + icon, + ariaLabel, + message, + actions, + onClose: () => { + if (isVirtualWorkspace) { + this.storageService.store(BANNER_VIRTUAL_WORKSPACE_DISMISSED_KEY, true, StorageScope.WORKSPACE, StorageTarget.MACHINE); + } + + if (restrictedMode) { + this.storageService.store(BANNER_RESTRICTED_MODE_DISMISSED_KEY, true, StorageScope.WORKSPACE, StorageTarget.MACHINE); + } + } }; } @@ -245,10 +317,20 @@ export class WorkspaceTrustRequestHandler extends Disposable implements IWorkben private updateWorkbenchIndicators(trusted: boolean): void { this.updateStatusbarEntry(trusted); - if (!trusted) { - this.bannerService.show(this.getBannerItem()); - } else { - this.bannerService.hide(BANNER_RESTRICTED_MODE); + + const isVirtualWorkspace = getVirtualWorkspaceScheme(this.workspaceContextService.getWorkspace()) !== undefined; + const bannerItem = this.getBannerItem(isVirtualWorkspace, !trusted); + + if (bannerItem) { + if (!isVirtualWorkspace) { + if (!trusted) { + this.bannerService.show(bannerItem); + } else { + this.bannerService.hide(BANNER_RESTRICTED_MODE); + } + } else { + this.bannerService.show(bannerItem); + } } } diff --git a/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.css b/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.css index 63f05bb1fd4..b26d4cc9aa5 100644 --- a/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.css +++ b/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.css @@ -217,8 +217,7 @@ } .workspace-trust-editor .workspace-trust-settings { - padding-top: 20px; - padding-bottom: 20px; + padding: 20px 14px; } .workspace-trust-editor .workspace-trust-settings .workspace-trusted-folders-title { @@ -268,3 +267,14 @@ padding-left: 8px; padding-right: 8px; } + +.workspace-trust-editor .workspace-trust-settings .monaco-table-tr .monaco-table-td .actions .monaco-action-bar { + display: none; + flex: 1; +} + +.workspace-trust-editor .workspace-trust-settings .monaco-list-row.selected .monaco-table-tr .monaco-table-td .actions .monaco-action-bar, +.workspace-trust-editor .workspace-trust-settings .monaco-table .monaco-list-row.focused .monaco-table-tr .monaco-table-td .actions .monaco-action-bar, +.workspace-trust-editor .workspace-trust-settings .monaco-list-row:hover .monaco-table-tr .monaco-table-td .actions .monaco-action-bar { + display: flex; +} diff --git a/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts b/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts index 537356d50b7..82016b8c8d1 100644 --- a/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts +++ b/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts @@ -119,8 +119,8 @@ class WorkspaceTrustedUrisTable extends Disposable { label: '', tooltip: '', weight: 0, - minimumWidth: 80, - maximumWidth: 80, + minimumWidth: 55, + maximumWidth: 55, templateId: TrustedUriActionsColumnRenderer.TEMPLATE_ID, project(row: ITrustedUriItem): ITrustedUriItem { return row; } }, @@ -175,9 +175,12 @@ class WorkspaceTrustedUrisTable extends Disposable { } } + private get currentWorkspaceUri(): URI { + return this.workspaceService.getWorkspace().folders[0]?.uri || URI.file('/'); + } + private get trustedUriEntries(): ITrustedUriItem[] { const currentWorkspace = this.workspaceService.getWorkspace(); - const currentWorkspaceUri = currentWorkspace.folders[0]?.uri || URI.file('/'); const currentWorkspaceUris = currentWorkspace.folders.map(folder => folder.uri); if (currentWorkspace.configuration) { currentWorkspaceUris.push(currentWorkspace.configuration); @@ -196,7 +199,7 @@ class WorkspaceTrustedUrisTable extends Disposable { parentOfWorkspaceItem: relatedToCurrentWorkspace }; }); - entries.push({ uri: currentWorkspaceUri, entryType: TrustedUriItemType.Add, parentOfWorkspaceItem: false }); + entries.push({ uri: this.currentWorkspaceUri, entryType: TrustedUriItemType.Add, parentOfWorkspaceItem: false }); return entries; } @@ -233,7 +236,9 @@ class WorkspaceTrustedUrisTable extends Disposable { } async edit(item: ITrustedUriItem) { - if (item.uri.scheme === Schemas.file || item.uri.scheme === Schemas.vscodeRemote) { + const canUseOpenDialog = item.uri.scheme === Schemas.file || + (item.uri.scheme === this.currentWorkspaceUri.scheme && this.uriService.extUri.isEqualAuthority(this.currentWorkspaceUri.authority, item.uri.authority)); + if (canUseOpenDialog) { const uri = await this.fileDialogService.showOpenDialog({ canSelectFiles: false, canSelectFolders: true, diff --git a/src/vs/workbench/services/banner/browser/bannerService.ts b/src/vs/workbench/services/banner/browser/bannerService.ts index 859bf6035e8..15f9fe15afd 100644 --- a/src/vs/workbench/services/banner/browser/bannerService.ts +++ b/src/vs/workbench/services/banner/browser/bannerService.ts @@ -7,16 +7,15 @@ import { Codicon } from 'vs/base/common/codicons'; import { MarkdownString } from 'vs/base/common/htmlContent'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { ILinkDescriptor } from 'vs/platform/opener/browser/link'; -import { StorageScope } from 'vs/platform/storage/common/storage'; export interface IBannerItem { readonly id: string; readonly icon: Codicon; readonly message: string | MarkdownString; - readonly scope?: StorageScope; /* Used to remember that the banner has been closed. */ readonly actions?: ILinkDescriptor[]; readonly ariaLabel?: string; + readonly onClose?: () => void; } export const IBannerService = createDecorator('bannerService'); diff --git a/src/vs/workbench/services/editor/browser/editorOverrideService.ts b/src/vs/workbench/services/editor/browser/editorOverrideService.ts index 990a4acf6d9..45c897382db 100644 --- a/src/vs/workbench/services/editor/browser/editorOverrideService.ts +++ b/src/vs/workbench/services/editor/browser/editorOverrideService.ts @@ -40,9 +40,13 @@ type ContributionPoints = Array; export class EditorOverrideService extends Disposable implements IEditorOverrideService { readonly _serviceBrand: undefined; + // Constants private static readonly configureDefaultID = 'promptOpenWith.configureDefault'; - private _contributionPoints: Map = new Map(); private static readonly overrideCacheStorageID = 'editorOverrideService.cache'; + private static readonly conflictingDefaultsStorageID = 'editorOverrideService.conflictingDefaults'; + + // Data Stores + private _contributionPoints: Map = new Map(); private cache: Set | undefined; constructor( @@ -129,8 +133,8 @@ export class EditorOverrideService extends Disposable implements IEditorOverride } const input = await this.doOverrideEditorInput(editor, options, group, selectedContribution); if (conflictingDefault && input) { - // Wait one second to give the user ample time to see the current editor then ask them to configure a default - this.doHandleConflictingDefaults(selectedContribution.editorInfo.label, input.editor, input.options ?? options, group); + // Show the conflicting default dialog + await this.doHandleConflictingDefaults(selectedContribution.editorInfo.label, input.editor, input.options ?? options, group); } // Add the group as we might've changed it with the quickpick @@ -367,12 +371,23 @@ export class EditorOverrideService extends Disposable implements IEditorOverride } private async doHandleConflictingDefaults(editorName: string, currentEditor: IContributedEditorInput, options: IEditorOptions | undefined, group: IEditorGroup) { - const makeCurrentEditorDefault = () => { - const viewType = currentEditor.viewType; - if (viewType) { - this.updateUserAssociations(`*${extname(currentEditor.resource!)}`, viewType); - } + type StoredChoice = { + [key: string]: string[]; }; + const contributionPoints = this.findMatchingContributions(currentEditor.resource!); + const storedChoices: StoredChoice = JSON.parse(this.storageService.get(EditorOverrideService.conflictingDefaultsStorageID, StorageScope.GLOBAL, '{}')); + const globForResource = `*${extname(currentEditor.resource!)}`; + // Writes to the storage service that a choice has been made for the currently installed editors + const writeCurrentEditorsToStorage = () => { + storedChoices[globForResource] = []; + contributionPoints.forEach(contrib => storedChoices[globForResource].push(contrib.editorInfo.id)); + this.storageService.store(EditorOverrideService.conflictingDefaultsStorageID, JSON.stringify(storedChoices), StorageScope.GLOBAL, StorageTarget.MACHINE); + }; + + // If the user has already made a choice for this editor we don't want to ask them again + if (storedChoices[globForResource]?.find(editorID => editorID === currentEditor.viewType)) { + return; + } const handle = this.notificationService.prompt(Severity.Warning, localize('editorOverride.conflictingDefaults', 'There are multiple default editors available for the resource.'), @@ -400,12 +415,12 @@ export class EditorOverrideService extends Disposable implements IEditorOverride }, { label: localize('editorOverride.keepDefault', 'Keep {0}', editorName), - run: makeCurrentEditorDefault + run: writeCurrentEditorsToStorage } ]); // If the user pressed X we assume they want to keep the current editor as default const onCloseListener = handle.onDidClose(() => { - makeCurrentEditorDefault(); + writeCurrentEditorsToStorage(); onCloseListener.dispose(); }); } diff --git a/src/vs/workbench/services/workspaces/common/workspaceTrust.ts b/src/vs/workbench/services/workspaces/common/workspaceTrust.ts index c49d8f6fade..7f134cebfa1 100644 --- a/src/vs/workbench/services/workspaces/common/workspaceTrust.ts +++ b/src/vs/workbench/services/workspaces/common/workspaceTrust.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { Codicon } from 'vs/base/common/codicons'; import { Emitter } from 'vs/base/common/event'; import { splitName } from 'vs/base/common/labels'; import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; @@ -389,6 +390,7 @@ export class WorkspaceTrustRequestService extends Disposable implements IWorkspa const result = await this.dialogService.show(Severity.Info, localize('openLooseFileMesssage', "Are you sure you want to open these files?"), [localize('open', "Open"), localize('newWindow', "Open in New Window"), localize('cancel', "Cancel")], { detail: localize('openLooseFileDetails', "You are trying to open untrusted files into the current window which is trusted. How would you like to continue?"), cancelId: 2, + custom: { icon: Codicon.shield } }); switch (result.choice) { diff --git a/yarn.lock b/yarn.lock index 02c65997a24..8359b7fa542 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8692,9 +8692,9 @@ sparkles@^1.0.0: integrity sha512-dSO0DDYUahUt/0/pD/Is3VIm5TGJjludZ0HVymmhYF6eNA53PVLhnUk0znSYbH8IYBuJdCE+1luR22jNLMaQdw== spdlog@^0.13.0: - version "0.13.4" - resolved "https://registry.yarnpkg.com/spdlog/-/spdlog-0.13.4.tgz#7393d436f077fca1d07500741e50cbf8928a838a" - integrity sha512-tdzk9ysc640emskx+pE/A2JdJ5IAr440ZIsNjRlD9aPK6U6IQ94VUGpl7u0NHamAB8O1H7RxLgtHyXT32V+RaA== + version "0.13.5" + resolved "https://registry.yarnpkg.com/spdlog/-/spdlog-0.13.5.tgz#a31027dcccbe032e9a53579f42cb45428af08bad" + integrity sha512-D1xA5tRXw7eZOoFBCAnOxCxLN3JpHVDjpPJG/xjJ0nFZvtfOUTAzK66MVxJCDht/ZFwjLcBAltvzjfz4JTuSEw== dependencies: bindings "^1.5.0" mkdirp "^0.5.5"