diff --git a/.github/subscribers.json b/.github/subscribers.json new file mode 100644 index 00000000000..89dee80d4a9 --- /dev/null +++ b/.github/subscribers.json @@ -0,0 +1,7 @@ +{ + "label-to-subscribe-to": [ + "list of usernames to subscribe", + "such as:", + "JacksonKearl" + ] +} diff --git a/.github/workflows/author-verified.yml b/.github/workflows/author-verified.yml index 167f27a5490..03cd7cedbef 100644 --- a/.github/workflows/author-verified.yml +++ b/.github/workflows/author-verified.yml @@ -17,7 +17,7 @@ jobs: uses: actions/checkout@v2 with: repository: 'microsoft/vscode-github-triage-actions' - ref: v34 + ref: v35 path: ./actions - name: Install Actions if: github.event_name != 'issues' || contains(github.event.issue.labels.*.name, 'author-verification-requested') diff --git a/.github/workflows/commands.yml b/.github/workflows/commands.yml index 61c82aa73ca..f61d114b157 100644 --- a/.github/workflows/commands.yml +++ b/.github/workflows/commands.yml @@ -13,7 +13,7 @@ jobs: with: repository: 'microsoft/vscode-github-triage-actions' path: ./actions - ref: v34 + ref: v35 - name: Install Actions run: npm install --production --prefix ./actions - name: Run Commands diff --git a/.github/workflows/deep-classifier-monitor.yml b/.github/workflows/deep-classifier-monitor.yml index 9425ae0f1f2..9d4c54dc6be 100644 --- a/.github/workflows/deep-classifier-monitor.yml +++ b/.github/workflows/deep-classifier-monitor.yml @@ -11,7 +11,7 @@ jobs: uses: actions/checkout@v2 with: repository: 'microsoft/vscode-github-triage-actions' - ref: v34 + ref: v35 path: ./actions - name: Install Actions run: npm install --production --prefix ./actions diff --git a/.github/workflows/deep-classifier-runner.yml b/.github/workflows/deep-classifier-runner.yml index 2f598745e49..c3d6b292157 100644 --- a/.github/workflows/deep-classifier-runner.yml +++ b/.github/workflows/deep-classifier-runner.yml @@ -13,7 +13,7 @@ jobs: uses: actions/checkout@v2 with: repository: 'microsoft/vscode-github-triage-actions' - ref: v34 + ref: v35 path: ./actions - name: Install Actions run: npm install --production --prefix ./actions diff --git a/.github/workflows/deep-classifier-scraper.yml b/.github/workflows/deep-classifier-scraper.yml index b9cb6ec3cd3..22b23b5d6fe 100644 --- a/.github/workflows/deep-classifier-scraper.yml +++ b/.github/workflows/deep-classifier-scraper.yml @@ -11,7 +11,7 @@ jobs: uses: actions/checkout@v2 with: repository: 'microsoft/vscode-github-triage-actions' - ref: v34 + ref: v35 path: ./actions - name: Install Actions run: npm install --production --prefix ./actions diff --git a/.github/workflows/english-please.yml b/.github/workflows/english-please.yml index fdfd548291d..6cf4d65455c 100644 --- a/.github/workflows/english-please.yml +++ b/.github/workflows/english-please.yml @@ -13,7 +13,7 @@ jobs: uses: actions/checkout@v2 with: repository: 'microsoft/vscode-github-triage-actions' - ref: v34 + ref: v35 path: ./actions - name: Install Actions if: contains(github.event.issue.labels.*.name, '*english-please') diff --git a/.github/workflows/feature-request.yml b/.github/workflows/feature-request.yml index 12af1423506..6cdf0bd6fc3 100644 --- a/.github/workflows/feature-request.yml +++ b/.github/workflows/feature-request.yml @@ -18,7 +18,7 @@ jobs: with: repository: 'microsoft/vscode-github-triage-actions' path: ./actions - ref: v34 + ref: v35 - name: Install Actions if: github.event_name != 'issues' || contains(github.event.issue.labels.*.name, 'feature-request') run: npm install --production --prefix ./actions diff --git a/.github/workflows/latest-release-monitor.yml b/.github/workflows/latest-release-monitor.yml index a5777e3a7f1..fb0a8320088 100644 --- a/.github/workflows/latest-release-monitor.yml +++ b/.github/workflows/latest-release-monitor.yml @@ -14,7 +14,7 @@ jobs: with: repository: 'microsoft/vscode-github-triage-actions' path: ./actions - ref: v34 + ref: v35 - name: Install Actions run: npm install --production --prefix ./actions - name: Install Storage Module diff --git a/.github/workflows/locker.yml b/.github/workflows/locker.yml index 64ac30d3717..2d21b96b833 100644 --- a/.github/workflows/locker.yml +++ b/.github/workflows/locker.yml @@ -14,7 +14,7 @@ jobs: with: repository: 'microsoft/vscode-github-triage-actions' path: ./actions - ref: v34 + ref: v35 - name: Install Actions run: npm install --production --prefix ./actions - name: Run Locker diff --git a/.github/workflows/needs-more-info-closer.yml b/.github/workflows/needs-more-info-closer.yml index 018caf24fe0..dae985ca35f 100644 --- a/.github/workflows/needs-more-info-closer.yml +++ b/.github/workflows/needs-more-info-closer.yml @@ -14,7 +14,7 @@ jobs: with: repository: 'microsoft/vscode-github-triage-actions' path: ./actions - ref: v34 + ref: v35 - name: Install Actions run: npm install --production --prefix ./actions - name: Run Needs More Info Closer diff --git a/.github/workflows/on-label.yml b/.github/workflows/on-label.yml index 446c041ebd3..e51b9ac740f 100644 --- a/.github/workflows/on-label.yml +++ b/.github/workflows/on-label.yml @@ -11,7 +11,7 @@ jobs: uses: actions/checkout@v2 with: repository: 'microsoft/vscode-github-triage-actions' - ref: v34 + ref: v35 path: ./actions - name: Install Actions run: npm install --production --prefix ./actions @@ -40,6 +40,13 @@ jobs: token: ${{secrets.VSCODE_ISSUE_TRIAGE_BOT_PAT}} config-path: commands + # only here. + - name: Run Subscribers + uses: ./actions/topic-subscribe + with: + appInsightsKey: ${{secrets.TRIAGE_ACTIONS_APP_INSIGHTS}} + token: ${{secrets.VSCODE_ISSUE_TRIAGE_BOT_PAT}} + config-path: subscribers # source of truth in ./feature-request.yml - name: Run Feature Request Manager diff --git a/.github/workflows/on-open.yml b/.github/workflows/on-open.yml index e4f0434dbdc..e985909b446 100644 --- a/.github/workflows/on-open.yml +++ b/.github/workflows/on-open.yml @@ -11,7 +11,7 @@ jobs: uses: actions/checkout@v2 with: repository: 'microsoft/vscode-github-triage-actions' - ref: v34 + ref: v35 path: ./actions - name: Install Actions run: npm install --production --prefix ./actions diff --git a/.github/workflows/release-pipeline-labeler.yml b/.github/workflows/release-pipeline-labeler.yml index 14f74f581dd..a52b895e51c 100644 --- a/.github/workflows/release-pipeline-labeler.yml +++ b/.github/workflows/release-pipeline-labeler.yml @@ -13,7 +13,7 @@ jobs: uses: actions/checkout@v2 with: repository: 'microsoft/vscode-github-triage-actions' - ref: v34 + ref: v35 path: ./actions - name: Checkout Repo if: github.event_name != 'issues' diff --git a/.github/workflows/test-plan-item-validator.yml b/.github/workflows/test-plan-item-validator.yml index 4365afa03e5..7a173b0e2f3 100644 --- a/.github/workflows/test-plan-item-validator.yml +++ b/.github/workflows/test-plan-item-validator.yml @@ -14,7 +14,7 @@ jobs: with: repository: 'microsoft/vscode-github-triage-actions' path: ./actions - ref: v34 + ref: v35 - name: Install Actions if: contains(github.event.issue.labels.*.name, 'testplan-item') || contains(github.event.issue.labels.*.name, 'invalid-testplan-item') run: npm install --production --prefix ./actions diff --git a/.vscode/searches/TrustedTypes.code-search b/.vscode/searches/TrustedTypes.code-search index 2112eb183c8..edd2d713940 100644 --- a/.vscode/searches/TrustedTypes.code-search +++ b/.vscode/searches/TrustedTypes.code-search @@ -4,7 +4,7 @@ # Excluding: *.test.ts # ContextLines: 3 -57 results - 39 files +42 results - 25 files src/vs/base/browser/markdownRenderer.ts: 156 const strValue = values[0]; @@ -23,15 +23,6 @@ src/vs/base/browser/markdownRenderer.ts: 243 // allowedTags should included everything that markdown renders to. 244 // Since we have our own sanitize function for marked, it's possible we missed some tag so let insane make sure. -src/vs/base/browser/ui/codicons/codiconStyles.ts: - 20 for (let c of iconRegistry.all) { - 21 rules.push(formatRule(c)); - 22 } - 23: codiconStyleSheet.innerHTML = rules.join('\n'); - 24 } - 25 - 26 const delayer = new RunOnceScheduler(updateAll, 0); - src/vs/base/browser/ui/contextview/contextview.ts: 157 this.shadowRootHostElement = DOM.$('.shadow-root-host'); 158 this.container.appendChild(this.shadowRootHostElement); @@ -41,15 +32,6 @@ src/vs/base/browser/ui/contextview/contextview.ts: 162 ${SHADOW_ROOT_CSS} 163 -src/vs/base/browser/ui/list/listWidget.ts: - 826 - 827 const newStyles = content.join('\n'); - 828 if (newStyles !== this.styleElement.innerHTML) { - 829: this.styleElement.innerHTML = newStyles; - 830 } - 831 } - 832 } - src/vs/base/browser/ui/menu/menu.ts: 229 private initializeStyleSheet(container: HTMLElement): void { 230 if (isInShadowDOM(container)) { @@ -74,15 +56,6 @@ src/vs/base/browser/ui/menu/menubar.ts: 555 556 let mnemonicMatches = MENU_MNEMONIC_REGEX.exec(label); -src/vs/base/browser/ui/sash/sash.ts: - 266 } - 267 } - 268 - 269: style.innerHTML = `* { cursor: ${cursor} !important; }`; - 270 }; - 271 - 272 const disposables = new DisposableStore(); - src/vs/base/browser/ui/selectBox/selectBoxCustom.ts: 367 content.push(`.monaco-select-box-dropdown-container > .select-box-dropdown-list-container .monaco-list .monaco-list-row.option-disabled:hover { outline: none !important; }`); 368 } @@ -92,22 +65,13 @@ src/vs/base/browser/ui/selectBox/selectBoxCustom.ts: 372 this.applyStyles(); 373 } - 705 }); - 706 - 707 - 708: container.innerHTML = this.options[longest].text + (!!this.options[longest].decoratorRight ? (this.options[longest].decoratorRight + ' ') : ''); - 709 elementWidth = dom.getTotalWidth(container); - 710 } - 711 - -src/vs/base/browser/ui/tree/abstractTree.ts: - 1476 - 1477 const newStyles = content.join('\n'); - 1478 if (newStyles !== this.styleElement.innerHTML) { - 1479: this.styleElement.innerHTML = newStyles; - 1480 } - 1481 - 1482 this.view.style(styles); + 680 }); + 681 + 682 + 683: container.innerHTML = this.options[longest].text + (!!this.options[longest].decoratorRight ? (this.options[longest].decoratorRight + ' ') : ''); + 684 elementWidth = dom.getTotalWidth(container); + 685 } + 686 src/vs/base/parts/quickinput/browser/quickInput.ts: 1693 } @@ -227,29 +191,29 @@ src/vs/editor/standalone/browser/colorizer.ts: 46 } src/vs/editor/standalone/browser/standaloneThemeServiceImpl.ts: - 211 if (!this._globalStyleElement) { - 212 this._globalStyleElement = dom.createStyleSheet(); - 213 this._globalStyleElement.className = 'monaco-colors'; - 214: this._globalStyleElement.innerHTML = this._css; - 215 this._styleElements.push(this._globalStyleElement); - 216 } - 217 return Disposable.None; + 212 if (!this._globalStyleElement) { + 213 this._globalStyleElement = dom.createStyleSheet(); + 214 this._globalStyleElement.className = 'monaco-colors'; + 215: this._globalStyleElement.innerHTML = this._css; + 216 this._styleElements.push(this._globalStyleElement); + 217 } + 218 return Disposable.None; - 220 private _registerShadowDomContainer(domNode: HTMLElement): IDisposable { - 221 const styleElement = dom.createStyleSheet(domNode); - 222 styleElement.className = 'monaco-colors'; - 223: styleElement.innerHTML = this._css; - 224 this._styleElements.push(styleElement); - 225 return { - 226 dispose: () => { + 221 private _registerShadowDomContainer(domNode: HTMLElement): IDisposable { + 222 const styleElement = dom.createStyleSheet(domNode); + 223 styleElement.className = 'monaco-colors'; + 224: styleElement.innerHTML = this._css; + 225 this._styleElements.push(styleElement); + 226 return { + 227 dispose: () => { - 290 ruleCollector.addRule(generateTokensCSSForColorMap(colorMap)); - 291 - 292 this._css = cssRules.join('\n'); - 293: this._styleElements.forEach(styleElement => styleElement.innerHTML = this._css); - 294 - 295 TokenizationRegistry.setColorMap(colorMap); - 296 this._onColorThemeChange.fire(theme); + 291 ruleCollector.addRule(generateTokensCSSForColorMap(colorMap)); + 292 + 293 this._css = cssRules.join('\n'); + 294: this._styleElements.forEach(styleElement => styleElement.innerHTML = this._css); + 295 + 296 TokenizationRegistry.setColorMap(colorMap); + 297 this._onColorThemeChange.fire(theme); src/vs/editor/test/browser/controller/imeTester.ts: 55 let content = this._model.getModelLineContent(i); @@ -268,24 +232,6 @@ src/vs/editor/test/browser/controller/imeTester.ts: 74 75 let startBtn = document.createElement('button'); -src/vs/workbench/browser/parts/compositePart.ts: - 414 const $this = this; - 415 return { - 416 updateTitle: (id, title, keybinding) => { - 417: titleLabel.innerHTML = strings.escape(title); - 418 titleLabel.title = keybinding ? nls.localize('titleTooltip', "{0} ({1})", title, keybinding) : title; - 419 }, - 420 - -src/vs/workbench/browser/parts/statusbar/statusbarPart.ts: - 672 this.styleElement = createStyleSheet(container); - 673 } - 674 - 675: this.styleElement.innerHTML = `.monaco-workbench .part.statusbar > .items-container > .statusbar-item.has-beak:before { border-bottom-color: ${backgroundColor}; }`; - 676 } - 677 - 678 private doCreateStatusItem(id: string, alignment: StatusbarAlignment, ...extraClasses: string[]): HTMLElement { - src/vs/workbench/contrib/comments/browser/commentsView.ts: 109 content.push(`.comments-panel .comments-panel-container .text code { color: ${codeTextForegroundColor}; }`); 110 } @@ -321,66 +267,30 @@ src/vs/workbench/contrib/debug/browser/repl.ts: 311 font-size: ${fontSize}px; 312 font-family: ${fontFamily}; -src/vs/workbench/contrib/debug/browser/statusbarColorProvider.ts: - 84 this.styleElement = createStyleSheet(container); - 85 } - 86 - 87: this.styleElement.innerHTML = `.monaco-workbench .part.statusbar > .items-container > .statusbar-item.has-beak:before { border-bottom-color: ${backgroundColor} !important; }`; - 88 } - 89 - 90 private getColorKey(noFolderColor: string, debuggingColor: string, normalColor: string): string { - -src/vs/workbench/contrib/files/browser/views/explorerView.ts: - 812 - 813 const newStyles = content.join('\n'); - 814 if (newStyles !== this.styleElement.innerHTML) { - 815: this.styleElement.innerHTML = newStyles; - 816 } - 817 } - 818 - -src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffList.ts: - 222 - 223 const newStyles = content.join('\n'); - 224 if (newStyles !== this.styleElement.innerHTML) { - 225: this.styleElement.innerHTML = newStyles; - 226 } - 227 } - 228 } - -src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts: - 1033 - 1034 const newStyles = content.join('\n'); - 1035 if (newStyles !== this.styleElement.innerHTML) { - 1036: this.styleElement.innerHTML = newStyles; - 1037 } - 1038 } - 1039 - src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts: - 443 - 444 private getMarkdownDragImage(templateData: MarkdownCellRenderTemplate): HTMLElement { - 445 const dragImageContainer = DOM.$('.cell-drag-image.monaco-list-row.focused.markdown-cell-row'); - 446: dragImageContainer.innerHTML = templateData.container.outerHTML; - 447 - 448 // Remove all rendered content nodes after the - 449 const markdownContent = dragImageContainer.querySelector('.cell.markdown')!; + 454 + 455 private getMarkdownDragImage(templateData: MarkdownCellRenderTemplate): HTMLElement { + 456 const dragImageContainer = DOM.$('.cell-drag-image.monaco-list-row.focused.markdown-cell-row'); + 457: dragImageContainer.innerHTML = templateData.container.outerHTML; + 458 + 459 // Remove all rendered content nodes after the + 460 const markdownContent = dragImageContainer.querySelector('.cell.markdown')!; - 587 - 588 private getDragImageImpl(templateData: BaseCellRenderTemplate, editor: ICodeEditor, type: 'code' | 'markdown'): HTMLElement | null { - 589 const dragImageContainer = DOM.$(`.cell-drag-image.monaco-list-row.focused.${type}-cell-row`); - 590: dragImageContainer.innerHTML = templateData.container.innerHTML; - 591 - 592 const editorContainer = dragImageContainer.querySelector('.cell-editor-container'); - 593 if (!editorContainer) { + 598 + 599 private getDragImageImpl(templateData: BaseCellRenderTemplate, editor: ICodeEditor, type: 'code' | 'markdown'): HTMLElement | null { + 600 const dragImageContainer = DOM.$(`.cell-drag-image.monaco-list-row.focused.${type}-cell-row`); + 601: dragImageContainer.innerHTML = templateData.container.innerHTML; + 602 + 603 const editorContainer = dragImageContainer.querySelector('.cell-editor-container'); + 604 if (!editorContainer) { - 599 return null; - 600 } - 601 - 602: editorContainer.innerHTML = richEditorText; - 603 - 604 return dragImageContainer; - 605 } + 610 return null; + 611 } + 612 + 613: editorContainer.innerHTML = richEditorText; + 614 + 615 return dragImageContainer; + 616 } src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts: 375 addMouseoverListeners(outputNode, outputId); @@ -400,24 +310,6 @@ src/vs/workbench/contrib/sash/browser/sash.ts: 40 .monaco-sash.horizontal { cursor: ns-resize; left: 0; width: 100%; height: ${size}px; } 41 .monaco-sash:not(.disabled).orthogonal-start::before, .monaco-sash:not(.disabled).orthogonal-end::after { content: ' '; height: ${size * 2}px; width: ${size * 2}px; z-index: 100; display: block; cursor: all-scroll; position: absolute; } -src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts: - 1301 - 1302 private setViewState(state: IViewState): void { - 1303 this.viewState = state; - 1304: this.stylesheet.innerHTML = ` - 1305 .monaco-editor .dirty-diff-modified,.monaco-editor .dirty-diff-added{border-left-width:${state.width}px;} - 1306 .monaco-editor .dirty-diff-modified, .monaco-editor .dirty-diff-added, .monaco-editor .dirty-diff-deleted { - 1307 opacity: ${state.visibility === 'always' ? 1 : 0}; - -src/vs/workbench/contrib/watermark/browser/watermark.ts: - 141 const dd = dom.append(dl, $('dd')); - 142 const keybinding = new KeybindingLabel(dd, OS, { renderUnboundKeybindings: true }); - 143 keybinding.set(this.keybindingService.lookupKeybinding(entry.id)); - 144: dd.innerHTML = keybinding.element.outerHTML; - 145 }); - 146 }; - 147 - src/vs/workbench/contrib/webview/browser/webviewIconManager.ts: 61 } 62 } @@ -460,24 +352,3 @@ src/vs/workbench/contrib/welcome/walkThrough/browser/walkThroughPart.ts: 307 this.content.appendChild(innerContent); 308 309 model.snippets.forEach((snippet, i) => { - -src/vs/workbench/services/textMate/browser/abstractTextMateService.ts: - 312 grammarFactory.setTheme(theme, tokenColorMap); - 313 let colorMap = AbstractTextMateService._toColorMap(tokenColorMap); - 314 let cssRules = generateTokensCSSForColorMap(colorMap); - 315: this._styleElement.innerHTML = cssRules; - 316 TokenizationRegistry.setColorMap(colorMap); - 317 } - 318 - -src/vs/workbench/services/themes/browser/workbenchThemeService.ts: - 692 const elStyle = document.createElement('style'); - 693 elStyle.type = 'text/css'; - 694 elStyle.className = rulesClassName; - 695: elStyle.innerHTML = styleSheetContent; - 696 document.head.appendChild(elStyle); - 697 } else { - 698: (themeStyles[0]).innerHTML = styleSheetContent; - 699 } - 700 } - 701 diff --git a/.vscode/searches/es6.code-search b/.vscode/searches/es6.code-search index 6ab0d14c5ac..54d4f0ea795 100644 --- a/.vscode/searches/es6.code-search +++ b/.vscode/searches/es6.code-search @@ -5,21 +5,21 @@ 14 results - 4 files src/vs/base/browser/dom.ts: - 81 }; - 82 - 83: /** @deprecated ES6 - use classList*/ - 84 export const hasClass: (node: HTMLElement | SVGElement, className: string) => boolean = _classList.hasClass.bind(_classList); + 83 }; + 84 85: /** @deprecated ES6 - use classList*/ - 86 export const addClass: (node: HTMLElement | SVGElement, className: string) => void = _classList.addClass.bind(_classList); + 86 export const hasClass: (node: HTMLElement | SVGElement, className: string) => boolean = _classList.hasClass.bind(_classList); 87: /** @deprecated ES6 - use classList*/ - 88 export const addClasses: (node: HTMLElement | SVGElement, ...classNames: string[]) => void = _classList.addClasses.bind(_classList); + 88 export const addClass: (node: HTMLElement | SVGElement, className: string) => void = _classList.addClass.bind(_classList); 89: /** @deprecated ES6 - use classList*/ - 90 export const removeClass: (node: HTMLElement | SVGElement, className: string) => void = _classList.removeClass.bind(_classList); + 90 export const addClasses: (node: HTMLElement | SVGElement, ...classNames: string[]) => void = _classList.addClasses.bind(_classList); 91: /** @deprecated ES6 - use classList*/ - 92 export const removeClasses: (node: HTMLElement | SVGElement, ...classNames: string[]) => void = _classList.removeClasses.bind(_classList); + 92 export const removeClass: (node: HTMLElement | SVGElement, className: string) => void = _classList.removeClass.bind(_classList); 93: /** @deprecated ES6 - use classList*/ - 94 export const toggleClass: (node: HTMLElement | SVGElement, className: string, shouldHaveIt?: boolean) => void = _classList.toggleClass.bind(_classList); - 95 + 94 export const removeClasses: (node: HTMLElement | SVGElement, ...classNames: string[]) => void = _classList.removeClasses.bind(_classList); + 95: /** @deprecated ES6 - use classList*/ + 96 export const toggleClass: (node: HTMLElement | SVGElement, className: string, shouldHaveIt?: boolean) => void = _classList.toggleClass.bind(_classList); + 97 src/vs/base/common/arrays.ts: 401 @@ -66,8 +66,8 @@ src/vs/base/common/strings.ts: 170 */ 171 export function endsWith(haystack: string, needle: string): boolean { - 861 - 862 /** - 863: * @deprecated ES6 - 864 */ - 865 export function repeat(s: string, count: number): string { + 857 + 858 /** + 859: * @deprecated ES6 + 860 */ + 861 export function repeat(s: string, count: number): string { diff --git a/build/azure-pipelines/common/createAsset.ts b/build/azure-pipelines/common/createAsset.ts index 53c15a28fcf..d7e62629cb8 100644 --- a/build/azure-pipelines/common/createAsset.ts +++ b/build/azure-pipelines/common/createAsset.ts @@ -53,7 +53,7 @@ async function uploadBlob(blobService: azure.BlobService, quality: string, blobN } }; - await new Promise((c, e) => blobService.createBlockBlobFromLocalFile(quality, blobName, filePath, blobOptions, err => err ? e(err) : c())); + await new Promise((c, e) => blobService.createBlockBlobFromLocalFile(quality, blobName, filePath, blobOptions, err => err ? e(err) : c())); } function getEnv(name: string): string { diff --git a/build/azure-pipelines/common/publish-webview.ts b/build/azure-pipelines/common/publish-webview.ts index 143b61bb61a..b1947ebc024 100644 --- a/build/azure-pipelines/common/publish-webview.ts +++ b/build/azure-pipelines/common/publish-webview.ts @@ -17,7 +17,7 @@ const fileNames = [ ]; async function assertContainer(blobService: azure.BlobService, container: string): Promise { - await new Promise((c, e) => blobService.createContainerIfNotExists(container, { publicAccessLevel: 'blob' }, err => err ? e(err) : c())); + await new Promise((c, e) => blobService.createContainerIfNotExists(container, { publicAccessLevel: 'blob' }, err => err ? e(err) : c())); } async function doesBlobExist(blobService: azure.BlobService, container: string, blobName: string): Promise { @@ -33,7 +33,7 @@ async function uploadBlob(blobService: azure.BlobService, container: string, blo } }; - await new Promise((c, e) => blobService.createBlockBlobFromLocalFile(container, blobName, file, blobOptions, err => err ? e(err) : c())); + await new Promise((c, e) => blobService.createBlockBlobFromLocalFile(container, blobName, file, blobOptions, err => err ? e(err) : c())); } async function publish(commit: string, files: readonly string[]): Promise { diff --git a/build/gulpfile.vscode.linux.js b/build/gulpfile.vscode.linux.js index e8e0c1541d0..1e4665bf7c0 100644 --- a/build/gulpfile.vscode.linux.js +++ b/build/gulpfile.vscode.linux.js @@ -52,6 +52,11 @@ function prepareDebPackage(arch) { .pipe(replace('@@LICENSE@@', product.licenseName)) .pipe(rename('usr/share/appdata/' + product.applicationName + '.appdata.xml')); + const workspaceMime = gulp.src('resources/code-workspace.xml', { base: '.' }) + .pipe(replace('@@NAME_LONG@@', product.nameLong)) + .pipe(replace('@@NAME@@', product.applicationName)) + .pipe(rename('usr/share/mime/packages/' + product.applicationName + '-workspace.xml')); + const icon = gulp.src('resources/linux/code.png', { base: '.' }) .pipe(rename('usr/share/pixmaps/' + product.linuxIconName + '.png')); @@ -95,7 +100,7 @@ function prepareDebPackage(arch) { .pipe(replace('@@UPDATEURL@@', product.updateUrl || '@@UPDATEURL@@')) .pipe(rename('DEBIAN/postinst')); - const all = es.merge(control, postinst, postrm, prerm, desktops, appdata, icon, bash_completion, zsh_completion, code); + const all = es.merge(control, postinst, postrm, prerm, desktops, appdata, workspaceMime, icon, bash_completion, zsh_completion, code); return all.pipe(vfs.dest(destination)); }; @@ -143,6 +148,11 @@ function prepareRpmPackage(arch) { .pipe(replace('@@LICENSE@@', product.licenseName)) .pipe(rename('usr/share/appdata/' + product.applicationName + '.appdata.xml')); + const workspaceMime = gulp.src('resources/code-workspace.xml', { base: '.' }) + .pipe(replace('@@NAME_LONG@@', product.nameLong)) + .pipe(replace('@@NAME@@', product.applicationName)) + .pipe(rename('BUILD/usr/share/mime/packages/' + product.applicationName + '-workspace.xml')); + const icon = gulp.src('resources/linux/code.png', { base: '.' }) .pipe(rename('BUILD/usr/share/pixmaps/' + product.linuxIconName + '.png')); @@ -173,7 +183,7 @@ function prepareRpmPackage(arch) { const specIcon = gulp.src('resources/linux/rpm/code.xpm', { base: '.' }) .pipe(rename('SOURCES/' + product.applicationName + '.xpm')); - const all = es.merge(code, desktops, appdata, icon, bash_completion, zsh_completion, spec, specIcon); + const all = es.merge(code, desktops, appdata, workspaceMime, icon, bash_completion, zsh_completion, spec, specIcon); return all.pipe(vfs.dest(getRpmBuildPath(rpmArch))); }; diff --git a/build/lib/i18n.ts b/build/lib/i18n.ts index 89131b0129f..418a7b5c362 100644 --- a/build/lib/i18n.ts +++ b/build/lib/i18n.ts @@ -996,7 +996,7 @@ function createResource(project: string, slug: string, xlfFile: File, apiHostnam * https://dev.befoolish.co/tx-docs/public/projects/updating-content#what-happens-when-you-update-files */ function updateResource(project: string, slug: string, xlfFile: File, apiHostname: string, credentials: string): Promise { - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { const data = JSON.stringify({ content: xlfFile.contents.toString() }); const options = { hostname: apiHostname, diff --git a/build/lib/preLaunch.ts b/build/lib/preLaunch.ts index bd084f5fec5..57441870cc4 100644 --- a/build/lib/preLaunch.ts +++ b/build/lib/preLaunch.ts @@ -15,7 +15,7 @@ const yarn = process.platform === 'win32' ? 'yarn.cmd' : 'yarn'; const rootDir = path.resolve(__dirname, '..', '..'); function runProcess(command: string, args: ReadonlyArray = []) { - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { const child = spawn(command, args, { cwd: rootDir, stdio: 'inherit', env: process.env }); child.on('exit', err => !err ? resolve() : process.exit(err ?? 1)); child.on('error', reject); diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index 1da994a9d6d..447cf2ed2b2 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -6,7 +6,7 @@ import { lstat, Stats } from 'fs'; import * as os from 'os'; import * as path from 'path'; -import { commands, Disposable, LineChange, MessageOptions, OutputChannel, Position, ProgressLocation, QuickPickItem, Range, SourceControlResourceState, TextDocumentShowOptions, TextEditor, Uri, ViewColumn, window, workspace, WorkspaceEdit, WorkspaceFolder, TimelineItem, env } from 'vscode'; +import { commands, Disposable, LineChange, MessageOptions, OutputChannel, Position, ProgressLocation, QuickPickItem, Range, SourceControlResourceState, TextDocumentShowOptions, TextEditor, Uri, ViewColumn, window, workspace, WorkspaceEdit, WorkspaceFolder, TimelineItem, env, Selection } from 'vscode'; import TelemetryReporter from 'vscode-extension-telemetry'; import * as nls from 'vscode-nls'; import { Branch, GitErrorCodes, Ref, RefType, Status, CommitOptions, RemoteSourceProvider } from './api/git'; @@ -1002,6 +1002,9 @@ export class CommandCenter { } await this._stageChanges(textEditor, [changes[index]]); + + const firstStagedLine = changes[index].modifiedStartLineNumber - 1; + textEditor.selections = [new Selection(firstStagedLine, 0, firstStagedLine, 0)]; } @command('git.stageSelectedRanges', { diff: true }) @@ -1049,6 +1052,9 @@ export class CommandCenter { } await this._revertChanges(textEditor, [...changes.slice(0, index), ...changes.slice(index + 1)]); + + const firstStagedLine = changes[index].modifiedStartLineNumber - 1; + textEditor.selections = [new Selection(firstStagedLine, 0, firstStagedLine, 0)]; } @command('git.revertSelectedRanges', { diff: true }) @@ -1070,7 +1076,9 @@ export class CommandCenter { return; } + const selectionsBeforeRevert = textEditor.selections; await this._revertChanges(textEditor, selectedChanges); + textEditor.selections = selectionsBeforeRevert; } private async _revertChanges(textEditor: TextEditor, changes: LineChange[]): Promise { @@ -1083,7 +1091,6 @@ export class CommandCenter { const originalUri = toGitUri(modifiedUri, '~'); const originalDocument = await workspace.openTextDocument(originalUri); - const selectionsBeforeRevert = textEditor.selections; const visibleRangesBeforeRevert = textEditor.visibleRanges; const result = applyLineChanges(originalDocument, modifiedDocument, changes); @@ -1093,7 +1100,6 @@ export class CommandCenter { await modifiedDocument.save(); - textEditor.selections = selectionsBeforeRevert; textEditor.revealRange(visibleRangesBeforeRevert[0]); } diff --git a/extensions/git/src/decorationProvider.ts b/extensions/git/src/decorationProvider.ts index 2ed893ed766..ab6e5713e11 100644 --- a/extensions/git/src/decorationProvider.ts +++ b/extensions/git/src/decorationProvider.ts @@ -21,7 +21,7 @@ class GitIgnoreDecorationProvider implements DecorationProvider { constructor(private model: Model) { this.onDidChangeDecorations = fireEvent(anyEvent( - filterEvent(workspace.onDidSaveTextDocument, e => e.fileName.endsWith('.gitignore')), + filterEvent(workspace.onDidSaveTextDocument, e => /\.gitignore$|\.git\/info\/exclude$/.test(e.uri.path)), model.onDidOpenRepository, model.onDidCloseRepository )); diff --git a/extensions/typescript-language-features/src/tsServer/server.ts b/extensions/typescript-language-features/src/tsServer/server.ts index 6ad3d015679..fe8671ab906 100644 --- a/extensions/typescript-language-features/src/tsServer/server.ts +++ b/extensions/typescript-language-features/src/tsServer/server.ts @@ -318,7 +318,6 @@ class RequestRouter { 'open', 'updateOpen', 'configure', - 'configurePlugin', ]); constructor( @@ -490,7 +489,8 @@ export class SyntaxRoutingTsServer extends Disposable implements ITypeScriptServ private static readonly semanticCommands = new Set([ 'geterr', 'geterrForProject', - 'projectInfo' + 'projectInfo', + 'configurePlugin', ]); /** diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts index 8e2afa3ba13..01232a99c8c 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts @@ -349,7 +349,7 @@ import { doesNotThrow, equal, ok, deepEqual, throws } from 'assert'; term1Write.fire('write1'); // Wait until the data is written - await new Promise(resolve => { resolveOnceDataWritten = resolve; }); + await new Promise(resolve => { resolveOnceDataWritten = resolve; }); term1Close.fire(); diff --git a/extensions/vscode-notebook-tests/src/notebook.test.ts b/extensions/vscode-notebook-tests/src/notebook.test.ts index d549dc8dc3f..0547012433a 100644 --- a/extensions/vscode-notebook-tests/src/notebook.test.ts +++ b/extensions/vscode-notebook-tests/src/notebook.test.ts @@ -690,11 +690,18 @@ suite('notebook workflow', () => { await vscode.commands.executeCommand('notebook.focusTop'); const cell = editor.document.cells[0]; assert.equal(cell.outputs.length, 0); + + let metadataChangeEvent = getEventOncePromise(vscode.notebook.onDidChangeCellMetadata); cell.metadata.runnable = false; + await metadataChangeEvent; + await vscode.commands.executeCommand('notebook.cell.execute'); assert.equal(cell.outputs.length, 0, 'should not execute'); // not runnable, didn't work + metadataChangeEvent = getEventOncePromise(vscode.notebook.onDidChangeCellMetadata); cell.metadata.runnable = true; + await metadataChangeEvent; + await vscode.commands.executeCommand('notebook.cell.execute'); assert.equal(cell.outputs.length, 1, 'should execute'); // runnable, it worked @@ -711,11 +718,17 @@ suite('notebook workflow', () => { const cell = editor.document.cells[0]; assert.equal(cell.outputs.length, 0); + let metadataChangeEvent = getEventOncePromise(vscode.notebook.onDidChangeNotebookDocumentMetadata); editor.document.metadata.runnable = false; + await metadataChangeEvent; + await vscode.commands.executeCommand('notebook.execute'); assert.equal(cell.outputs.length, 0, 'should not execute'); // not runnable, didn't work + metadataChangeEvent = getEventOncePromise(vscode.notebook.onDidChangeNotebookDocumentMetadata); editor.document.metadata.runnable = true; + await metadataChangeEvent; + await vscode.commands.executeCommand('notebook.execute'); assert.equal(cell.outputs.length, 1, 'should execute'); // runnable, it worked @@ -1032,15 +1045,14 @@ suite('metadata', () => { }); suite('regression', () => { - test('microsoft/vscode-github-issue-notebooks#26. Insert template cell in the new empty document', async function () { - assertInitalState(); - const resource = await createRandomFile('', undefined, 'empty', '.vsctestnb'); - await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); - assert.equal(vscode.notebook.activeNotebookEditor !== undefined, true, 'notebook first'); - assert.equal(vscode.notebook.activeNotebookEditor!.selection?.document.getText(), ''); - assert.equal(vscode.notebook.activeNotebookEditor!.selection?.language, 'typescript'); - await saveFileAndCloseAll(resource); - }); + // test('microsoft/vscode-github-issue-notebooks#26. Insert template cell in the new empty document', async function () { + // assertInitalState(); + // await vscode.commands.executeCommand('workbench.action.files.newUntitledFile', { "viewType": "notebookCoreTest" }); + // assert.equal(vscode.notebook.activeNotebookEditor !== undefined, true, 'notebook first'); + // assert.equal(vscode.notebook.activeNotebookEditor!.selection?.document.getText(), ''); + // assert.equal(vscode.notebook.activeNotebookEditor!.selection?.language, 'typescript'); + // await vscode.commands.executeCommand('workbench.action.closeAllEditors'); + // }); test('#97830, #97764. Support switch to other editor types', async function () { assertInitalState(); @@ -1073,7 +1085,7 @@ suite('regression', () => { // now it's dirty, open the resource with notebook editor should open a new one await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); assert.notEqual(vscode.notebook.activeNotebookEditor, undefined, 'notebook first'); - assert.notEqual(vscode.window.activeTextEditor, undefined); + // assert.notEqual(vscode.window.activeTextEditor, undefined); await vscode.commands.executeCommand('workbench.action.closeAllEditors'); }); diff --git a/product.json b/product.json index 7651fc50bb5..e79fb4baf5e 100644 --- a/product.json +++ b/product.json @@ -76,7 +76,7 @@ }, { "name": "ms-vscode.js-debug-companion", - "version": "1.0.7", + "version": "1.0.8", "repo": "https://github.com/microsoft/vscode-js-debug-companion", "metadata": { "id": "99cb0b7f-7354-4278-b8da-6cc79972169d", diff --git a/resources/linux/code-workspace.xml b/resources/linux/code-workspace.xml new file mode 100644 index 00000000000..5a3e286ef43 --- /dev/null +++ b/resources/linux/code-workspace.xml @@ -0,0 +1,7 @@ + + + + @@NAME_LONG@@ Workspace + + + diff --git a/resources/linux/code.desktop b/resources/linux/code.desktop index b975e1094a2..62d6bfc47b4 100755 --- a/resources/linux/code.desktop +++ b/resources/linux/code.desktop @@ -8,7 +8,7 @@ Type=Application StartupNotify=false StartupWMClass=@@NAME_SHORT@@ Categories=Utility;TextEditor;Development;IDE; -MimeType=text/plain;inode/directory; +MimeType=text/plain;inode/directory;application/x-@@NAME@@-workspace; Actions=new-empty-window; Keywords=vscode; diff --git a/resources/linux/debian/postinst.template b/resources/linux/debian/postinst.template index 2c4a1fdd1d6..9f26b350999 100755 --- a/resources/linux/debian/postinst.template +++ b/resources/linux/debian/postinst.template @@ -18,6 +18,11 @@ if hash desktop-file-install 2>/dev/null; then desktop-file-install /usr/share/applications/@@NAME@@-url-handler.desktop fi +# Update mimetype database to pickup workspace mimetype +if hash update-mime-database 2>/dev/null; then + update-mime-database /usr/share/mime +fi + if [ "@@NAME@@" != "code-oss" ]; then # Remove the legacy bin command if this is the stable build if [ "@@NAME@@" = "code" ]; then diff --git a/resources/linux/debian/postrm.template b/resources/linux/debian/postrm.template index c43a2b16ae3..238a566c486 100755 --- a/resources/linux/debian/postrm.template +++ b/resources/linux/debian/postrm.template @@ -3,4 +3,9 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See License.txt in the project root for license information. -rm -f /usr/bin/@@NAME@@ \ No newline at end of file +rm -f /usr/bin/@@NAME@@ + +# Update mimetype database for removed workspace mimetype +if hash update-mime-database 2>/dev/null; then + update-mime-database /usr/share/mime +fi diff --git a/resources/linux/rpm/code.spec.template b/resources/linux/rpm/code.spec.template index 93e20cd4825..4c0b9db4718 100644 --- a/resources/linux/rpm/code.spec.template +++ b/resources/linux/rpm/code.spec.template @@ -23,6 +23,7 @@ mkdir -p %{buildroot}/usr/share/zsh/site-functions cp -r usr/share/@@NAME@@/* %{buildroot}/usr/share/@@NAME@@ cp -r usr/share/applications/@@NAME@@.desktop %{buildroot}/usr/share/applications cp -r usr/share/applications/@@NAME@@-url-handler.desktop %{buildroot}/usr/share/applications +cp -r usr/share/mime/packages/@@NAME@@-workspace.xml %{buildroot}/usr/share/mime/packages/@@NAME-workspace.xml cp -r usr/share/pixmaps/@@ICON@@.png %{buildroot}/usr/share/pixmaps cp usr/share/bash-completion/completions/@@NAME@@ %{buildroot}/usr/share/bash-completion/completions/@@NAME@@ cp usr/share/zsh/site-functions/_@@NAME@@ %{buildroot}/usr/share/zsh/site-functions/_@@NAME@@ @@ -46,17 +47,24 @@ ln -sf /usr/share/@@NAME@@/bin/@@NAME@@ %{_bindir}/@@NAME@@ # fi #fi +# Update mimetype database to pickup workspace mimetype +update-mime-database /usr/share/mime &> /dev/null || : + %postun if [ $1 = 0 ]; then rm -f /usr/bin/@@NAME@@ fi +# Update mimetype database for removed workspace mimetype +update-mime-database /usr/share/mime &> /dev/null || : + %files %defattr(-,root,root) /usr/share/@@NAME@@/ /usr/share/applications/@@NAME@@.desktop /usr/share/applications/@@NAME@@-url-handler.desktop +/usr/share/mime/packages/@@NAME-workspace.xml /usr/share/pixmaps/@@ICON@@.png /usr/share/bash-completion/completions/@@NAME@@ /usr/share/zsh/site-functions/_@@NAME@@ diff --git a/src/vs/base/browser/ui/codicons/codicon/codicon.ttf b/src/vs/base/browser/ui/codicons/codicon/codicon.ttf index 82acc8995b8..cb526553433 100644 Binary files a/src/vs/base/browser/ui/codicons/codicon/codicon.ttf and b/src/vs/base/browser/ui/codicons/codicon/codicon.ttf differ diff --git a/src/vs/base/browser/ui/codicons/codiconStyles.ts b/src/vs/base/browser/ui/codicons/codiconStyles.ts index b3dc12fb3f3..59dbdbe873a 100644 --- a/src/vs/base/browser/ui/codicons/codiconStyles.ts +++ b/src/vs/base/browser/ui/codicons/codiconStyles.ts @@ -20,7 +20,7 @@ function initialize() { for (let c of iconRegistry.all) { rules.push(formatRule(c)); } - codiconStyleSheet.innerHTML = rules.join('\n'); + codiconStyleSheet.textContent = rules.join('\n'); } const delayer = new RunOnceScheduler(updateAll, 0); diff --git a/src/vs/base/browser/ui/list/listWidget.ts b/src/vs/base/browser/ui/list/listWidget.ts index 252150b28d7..81fe5160a5e 100644 --- a/src/vs/base/browser/ui/list/listWidget.ts +++ b/src/vs/base/browser/ui/list/listWidget.ts @@ -824,10 +824,7 @@ export class DefaultStyleController implements IStyleController { content.push(`.monaco-list-type-filter { box-shadow: 1px 1px 1px ${styles.listMatchesShadow}; }`); } - const newStyles = content.join('\n'); - if (newStyles !== this.styleElement.innerHTML) { - this.styleElement.innerHTML = newStyles; - } + this.styleElement.textContent = content.join('\n'); } } diff --git a/src/vs/base/browser/ui/menu/menubar.ts b/src/vs/base/browser/ui/menu/menubar.ts index 65249946405..3a0c90d47e7 100644 --- a/src/vs/base/browser/ui/menu/menubar.ts +++ b/src/vs/base/browser/ui/menu/menubar.ts @@ -532,25 +532,31 @@ export class MenuBar extends Disposable { // Update the button label to reflect mnemonics if (this.options.enableMnemonics) { - let innerHtml = strings.escape(label); + let cleanLabel = strings.escape(label); // This is global so reset it MENU_ESCAPED_MNEMONIC_REGEX.lastIndex = 0; - let escMatch = MENU_ESCAPED_MNEMONIC_REGEX.exec(innerHtml); + let escMatch = MENU_ESCAPED_MNEMONIC_REGEX.exec(cleanLabel); // We can't use negative lookbehind so we match our negative and skip while (escMatch && escMatch[1]) { - escMatch = MENU_ESCAPED_MNEMONIC_REGEX.exec(innerHtml); + escMatch = MENU_ESCAPED_MNEMONIC_REGEX.exec(cleanLabel); } + const replaceDoubleEscapes = (str: string) => str.replace(/&&/g, '&'); + if (escMatch) { - innerHtml = `${innerHtml.substr(0, escMatch.index)}${innerHtml.substr(escMatch.index + escMatch[0].length)}`; + titleElement.innerText = ''; + titleElement.append( + strings.ltrim(replaceDoubleEscapes(cleanLabel.substr(0, escMatch.index)), ' '), + $('mnemonic', { 'aria-hidden': 'true' }, escMatch[3]), + strings.rtrim(replaceDoubleEscapes(cleanLabel.substr(escMatch.index + escMatch[0].length)), ' ') + ); + } else { + titleElement.innerText = replaceDoubleEscapes(cleanLabel).trim(); } - - innerHtml = innerHtml.replace(/&&/g, '&'); - titleElement.innerHTML = innerHtml; } else { - titleElement.innerHTML = cleanMenuLabel.replace(/&&/g, '&'); + titleElement.innerText = cleanMenuLabel.replace(/&&/g, '&'); } let mnemonicMatches = MENU_MNEMONIC_REGEX.exec(label); diff --git a/src/vs/base/browser/ui/sash/sash.ts b/src/vs/base/browser/ui/sash/sash.ts index 7941babc3f4..a8db08e032d 100644 --- a/src/vs/base/browser/ui/sash/sash.ts +++ b/src/vs/base/browser/ui/sash/sash.ts @@ -266,7 +266,7 @@ export class Sash extends Disposable { } } - style.innerHTML = `* { cursor: ${cursor} !important; }`; + style.textContent = `* { cursor: ${cursor} !important; }`; }; const disposables = new DisposableStore(); diff --git a/src/vs/base/browser/ui/tree/abstractTree.ts b/src/vs/base/browser/ui/tree/abstractTree.ts index 16e17fcf763..74ab08a9fba 100644 --- a/src/vs/base/browser/ui/tree/abstractTree.ts +++ b/src/vs/base/browser/ui/tree/abstractTree.ts @@ -1474,11 +1474,7 @@ export abstract class AbstractTree implements IDisposable content.push(`.monaco-list${suffix} .monaco-tl-indent > .indent-guide.active { border-color: ${styles.treeIndentGuidesStroke}; }`); } - const newStyles = content.join('\n'); - if (newStyles !== this.styleElement.innerHTML) { - this.styleElement.innerHTML = newStyles; - } - + this.styleElement.textContent = content.join('\n'); this.view.style(styles); } diff --git a/src/vs/base/common/codicons.ts b/src/vs/base/common/codicons.ts index d71d1bf68d5..926bfa41a35 100644 --- a/src/vs/base/common/codicons.ts +++ b/src/vs/base/common/codicons.ts @@ -477,6 +477,8 @@ export namespace Codicon { export const vmConnect = new Codicon('vm-connect', { character: '\\eba9' }); export const cloud = new Codicon('cloud', { character: '\\ebaa' }); export const merge = new Codicon('merge', { character: '\\ebab' }); + export const exportIcon = new Codicon('export', { character: '\\ebac' }); + export const graphLeft = new Codicon('graph-left', { character: '\\ebad' }); } diff --git a/src/vs/base/node/ps.ts b/src/vs/base/node/ps.ts index 0786b753dd2..31223d4a5d8 100644 --- a/src/vs/base/node/ps.ts +++ b/src/vs/base/node/ps.ts @@ -193,6 +193,11 @@ export function listProcesses(rootPid: number): Promise { processInfo.load = parseFloat(cpuUsage[i]); } + if (!rootItem) { + reject(new Error(`Root process ${rootPid} not found`)); + return; + } + resolve(rootItem); } }); @@ -228,7 +233,11 @@ export function listProcesses(rootPid: number): Promise { if (process.platform === 'linux') { calculateLinuxCpuUsage(); } else { - resolve(rootItem); + if (!rootItem) { + reject(new Error(`Root process ${rootPid} not found`)); + } else { + resolve(rootItem); + } } } }); diff --git a/src/vs/base/node/terminalEncoding.ts b/src/vs/base/node/terminalEncoding.ts index fdfc268c48a..32ff35ea028 100644 --- a/src/vs/base/node/terminalEncoding.ts +++ b/src/vs/base/node/terminalEncoding.ts @@ -40,7 +40,7 @@ const JSCHARDET_TO_ICONV_ENCODINGS: { [name: string]: string } = { const UTF8 = 'utf8'; export async function resolveTerminalEncoding(verbose?: boolean): Promise { - let rawEncodingPromise: Promise; + let rawEncodingPromise: Promise; // Support a global environment variable to win over other mechanics const cliEncodingEnv = process.env['VSCODE_CLI_ENCODING']; @@ -54,7 +54,7 @@ export async function resolveTerminalEncoding(verbose?: boolean): Promise(resolve => { + rawEncodingPromise = new Promise(resolve => { if (verbose) { console.log('Running "chcp" to detect terminal encoding...'); } diff --git a/src/vs/base/node/zip.ts b/src/vs/base/node/zip.ts index 8047aa275f7..a0708bf92df 100644 --- a/src/vs/base/node/zip.ts +++ b/src/vs/base/node/zip.ts @@ -12,6 +12,7 @@ import { mkdirp, rimraf } from 'vs/base/node/pfs'; import { open as _openZip, Entry, ZipFile } from 'yauzl'; import * as yazl from 'yazl'; import { CancellationToken } from 'vs/base/common/cancellation'; +import { assertIsDefined } from 'vs/base/common/types'; export interface IExtractOptions { overwrite?: boolean; @@ -161,24 +162,24 @@ function extractZip(zipfile: ZipFile, targetPath: string, options: IOptions, tok } function openZip(zipFile: string, lazy: boolean = false): Promise { - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { _openZip(zipFile, lazy ? { lazyEntries: true } : undefined!, (error?: Error, zipfile?: ZipFile) => { if (error) { reject(toExtractError(error)); } else { - resolve(zipfile); + resolve(assertIsDefined(zipfile)); } }); }); } function openZipStream(zipFile: ZipFile, entry: Entry): Promise { - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { zipFile.openReadStream(entry, (error?: Error, stream?: Readable) => { if (error) { reject(toExtractError(error)); } else { - resolve(stream); + resolve(assertIsDefined(stream)); } }); }); diff --git a/src/vs/base/parts/quickinput/browser/quickInput.ts b/src/vs/base/parts/quickinput/browser/quickInput.ts index d2ca01cf849..1061baac5ff 100644 --- a/src/vs/base/parts/quickinput/browser/quickInput.ts +++ b/src/vs/base/parts/quickinput/browser/quickInput.ts @@ -1416,7 +1416,7 @@ export class QuickInputController extends Disposable { } input(options: IInputOptions = {}, token: CancellationToken = CancellationToken.None): Promise { - return new Promise((resolve, reject) => { + return new Promise((resolve) => { if (token.isCancellationRequested) { resolve(undefined); return; diff --git a/src/vs/base/test/common/async.test.ts b/src/vs/base/test/common/async.test.ts index 9d133c91dfa..aa055d77c67 100644 --- a/src/vs/base/test/common/async.test.ts +++ b/src/vs/base/test/common/async.test.ts @@ -527,7 +527,7 @@ suite('Async', () => { r1Queue.queue(syncPromiseFactory); - return new Promise(c => setTimeout(() => c(), 0)).then(() => { + return new Promise(c => setTimeout(() => c(), 0)).then(() => { const r1Queue2 = queue.queueFor(URI.file('/some/path')); assert.notEqual(r1Queue, r1Queue2); // previous one got disposed after finishing }); diff --git a/src/vs/base/test/common/utils.ts b/src/vs/base/test/common/utils.ts index d77f9f786e2..9175dad6365 100644 --- a/src/vs/base/test/common/utils.ts +++ b/src/vs/base/test/common/utils.ts @@ -25,21 +25,21 @@ export class DeferredPromise { } public complete(value: T) { - return new Promise(resolve => { + return new Promise(resolve => { this.completeCallback(value); resolve(); }); } public error(err: any) { - return new Promise(resolve => { + return new Promise(resolve => { this.errorCallback(err); resolve(); }); } public cancel() { - new Promise(resolve => { + new Promise(resolve => { this.errorCallback(canceled()); resolve(); }); diff --git a/src/vs/code/node/cli.ts b/src/vs/code/node/cli.ts index 5f2d74a1c68..1cdfe65520f 100644 --- a/src/vs/code/node/cli.ts +++ b/src/vs/code/node/cli.ts @@ -138,7 +138,7 @@ export async function main(argv: string[]): Promise { child.stdout!.on('data', (data: Buffer) => console.log(data.toString('utf8').trim())); child.stderr!.on('data', (data: Buffer) => console.log(data.toString('utf8').trim())); - await new Promise(c => child.once('exit', () => c())); + await new Promise(c => child.once('exit', () => c())); }); } diff --git a/src/vs/editor/common/modes/supports/tokenization.ts b/src/vs/editor/common/modes/supports/tokenization.ts index 12566c3b00e..e28c23d39b4 100644 --- a/src/vs/editor/common/modes/supports/tokenization.ts +++ b/src/vs/editor/common/modes/supports/tokenization.ts @@ -395,7 +395,7 @@ export class ThemeTrieElement { } } -export function generateTokensCSSForColorMap(colorMap: Color[]): string { +export function generateTokensCSSForColorMap(colorMap: readonly Color[]): string { let rules: string[] = []; for (let i = 1, len = colorMap.length; i < len; i++) { let color = colorMap[i]; diff --git a/src/vs/editor/contrib/links/getLinks.ts b/src/vs/editor/contrib/links/getLinks.ts index 0ee40d5166e..be5c7416457 100644 --- a/src/vs/editor/contrib/links/getLinks.ts +++ b/src/vs/editor/contrib/links/getLinks.ts @@ -13,6 +13,7 @@ import { IModelService } from 'vs/editor/common/services/modelService'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { isDisposable, Disposable } from 'vs/base/common/lifecycle'; import { coalesce } from 'vs/base/common/arrays'; +import { assertType } from 'vs/base/common/types'; export class Link implements ILink { @@ -152,10 +153,13 @@ export function getLinks(model: ITextModel, token: CancellationToken): Promise => { - const [uri] = args; - if (!(uri instanceof URI)) { - return []; + let [uri, resolveCount] = args; + assertType(uri instanceof URI); + + if (typeof resolveCount !== 'number') { + resolveCount = 0; } + const model = accessor.get(IModelService).getModel(uri); if (!model) { return []; @@ -164,6 +168,12 @@ CommandsRegistry.registerCommand('_executeLinkProvider', async (accessor, ...arg if (!list) { return []; } + + // resolve links + for (let i = 0; i < Math.min(resolveCount, list.links.length); i++) { + await list.links[i].resolve(CancellationToken.None); + } + const result = list.links.slice(0); list.dispose(); return result; diff --git a/src/vs/editor/contrib/parameterHints/test/parameterHintsModel.test.ts b/src/vs/editor/contrib/parameterHints/test/parameterHintsModel.test.ts index 2d5e1967fe7..f6c75e1f194 100644 --- a/src/vs/editor/contrib/parameterHints/test/parameterHintsModel.test.ts +++ b/src/vs/editor/contrib/parameterHints/test/parameterHintsModel.test.ts @@ -290,7 +290,7 @@ suite('ParameterHintsModel', () => { hintsModel.trigger({ triggerKind: modes.SignatureHelpTriggerKind.Invoke }, 0); assert.strictEqual(-1, didRequestCancellationOf); - return new Promise((resolve, reject) => + return new Promise((resolve, reject) => hintsModel.onChangedHints(newParamterHints => { try { assert.strictEqual(0, didRequestCancellationOf); diff --git a/src/vs/editor/contrib/rename/test/onTypeRename.test.ts b/src/vs/editor/contrib/rename/test/onTypeRename.test.ts index 303b3554445..05dba5e4b62 100644 --- a/src/vs/editor/contrib/rename/test/onTypeRename.test.ts +++ b/src/vs/editor/contrib/rename/test/onTypeRename.test.ts @@ -104,7 +104,7 @@ suite('On type rename', () => { await operations(testEditor); - return new Promise((resolve) => { + return new Promise((resolve) => { setTimeout(() => { if (typeof expectedEndText === 'string') { assert.equal(editor.getModel()!.getValue(), expectedEndText); diff --git a/src/vs/editor/contrib/smartSelect/bracketSelections.ts b/src/vs/editor/contrib/smartSelect/bracketSelections.ts index 69bbb6eadea..3842ad87b54 100644 --- a/src/vs/editor/contrib/smartSelect/bracketSelections.ts +++ b/src/vs/editor/contrib/smartSelect/bracketSelections.ts @@ -19,8 +19,8 @@ export class BracketSelectionRangeProvider implements SelectionRangeProvider { result.push(bucket); const ranges = new Map>(); - await new Promise(resolve => BracketSelectionRangeProvider._bracketsRightYield(resolve, 0, model, position, ranges)); - await new Promise(resolve => BracketSelectionRangeProvider._bracketsLeftYield(resolve, 0, model, position, ranges, bucket)); + await new Promise(resolve => BracketSelectionRangeProvider._bracketsRightYield(resolve, 0, model, position, ranges)); + await new Promise(resolve => BracketSelectionRangeProvider._bracketsLeftYield(resolve, 0, model, position, ranges, bucket)); } return result; diff --git a/src/vs/platform/configuration/test/common/configurationService.test.ts b/src/vs/platform/configuration/test/common/configurationService.test.ts index 2d19fcf1308..ee4dcdfa773 100644 --- a/src/vs/platform/configuration/test/common/configurationService.test.ts +++ b/src/vs/platform/configuration/test/common/configurationService.test.ts @@ -89,7 +89,7 @@ suite('ConfigurationService', () => { test('trigger configuration change event when file does not exist', async () => { const testObject = disposables.add(new ConfigurationService(settingsResource, fileService)); await testObject.initialize(); - return new Promise(async (c, e) => { + return new Promise(async (c) => { disposables.add(Event.filter(testObject.onDidChangeConfiguration, e => e.source === ConfigurationTarget.USER)(() => { assert.equal(testObject.getValue('foo'), 'bar'); c(); @@ -104,7 +104,7 @@ suite('ConfigurationService', () => { await fileService.writeFile(settingsResource, VSBuffer.fromString('{ "foo": "bar" }')); await testObject.initialize(); - return new Promise((c, e) => { + return new Promise((c) => { disposables.add(Event.filter(testObject.onDidChangeConfiguration, e => e.source === ConfigurationTarget.USER)(async (e) => { assert.equal(testObject.getValue('foo'), 'barz'); c(); diff --git a/src/vs/platform/debug/electron-main/extensionHostDebugIpc.ts b/src/vs/platform/debug/electron-main/extensionHostDebugIpc.ts index 923305acd75..074e9ecb3e6 100644 --- a/src/vs/platform/debug/electron-main/extensionHostDebugIpc.ts +++ b/src/vs/platform/debug/electron-main/extensionHostDebugIpc.ts @@ -101,7 +101,7 @@ export class ElectronExtensionHostDebugBroadcastChannel extends Extens }); }); - await new Promise(r => server.listen(0, r)); + await new Promise(r => server.listen(0, r)); codeWindow.win.on('close', () => server.close()); return { rendererDebugPort: (server.address() as AddressInfo).port }; diff --git a/src/vs/platform/driver/browser/baseDriver.ts b/src/vs/platform/driver/browser/baseDriver.ts index 2b4ee06778e..90610713f9d 100644 --- a/src/vs/platform/driver/browser/baseDriver.ts +++ b/src/vs/platform/driver/browser/baseDriver.ts @@ -186,5 +186,5 @@ export abstract class BaseWindowDriver implements IWindowDriver { return { x, y }; } - abstract async openDevTools(): Promise; + abstract openDevTools(): Promise; } diff --git a/src/vs/platform/extensionManagement/node/extensionManagementService.ts b/src/vs/platform/extensionManagement/node/extensionManagementService.ts index 9b0fd4db47e..e6c3252ada1 100644 --- a/src/vs/platform/extensionManagement/node/extensionManagementService.ts +++ b/src/vs/platform/extensionManagement/node/extensionManagementService.ts @@ -45,7 +45,6 @@ import { IExtensionManifest, ExtensionType } from 'vs/platform/extensions/common import { ExtensionsDownloader } from 'vs/platform/extensionManagement/node/extensionDownloader'; import { ExtensionsScanner, IMetadata } from 'vs/platform/extensionManagement/node/extensionsScanner'; import { ExtensionsLifecycle } from 'vs/platform/extensionManagement/node/extensionLifecycle'; -import { IStringDictionary } from 'vs/base/common/collections'; const INSTALL_ERROR_UNSET_UNINSTALLED = 'unsetUninstalled'; const INSTALL_ERROR_DOWNLOADING = 'downloading'; @@ -66,7 +65,7 @@ export class ExtensionManagementService extends Disposable implements IExtension private readonly extensionsScanner: ExtensionsScanner; private reportedExtensions: Promise | undefined; private lastReportTimestamp = 0; - private readonly installingExtensions: Map> = new Map>(); + private readonly installingExtensions = new Map>(); private readonly uninstallingExtensions: Map> = new Map>(); private readonly manifestCache: ExtensionsManifestCache; private readonly extensionsDownloader: ExtensionsDownloader; @@ -105,16 +104,17 @@ export class ExtensionManagementService extends Disposable implements IExtension })); } - zip(extension: ILocalExtension): Promise { + async zip(extension: ILocalExtension): Promise { this.logService.trace('ExtensionManagementService#zip', extension.identifier.id); - return this.collectFiles(extension) - .then(files => zip(path.join(tmpdir(), generateUuid()), files)) - .then(path => URI.file(path)); + const files = await this.collectFiles(extension); + const location = await zip(path.join(tmpdir(), generateUuid()), files); + return URI.file(location); } - unzip(zipLocation: URI): Promise { + async unzip(zipLocation: URI): Promise { this.logService.trace('ExtensionManagementService#unzip', zipLocation.toString()); - return this.install(zipLocation).then(local => local.identifier); + const local = await this.install(zipLocation); + return local.identifier; } async getManifest(vsix: URI): Promise { @@ -123,7 +123,7 @@ export class ExtensionManagementService extends Disposable implements IExtension return getManifest(zipPath); } - private collectFiles(extension: ILocalExtension): Promise { + private async collectFiles(extension: ILocalExtension): Promise { const collectFilesFromDirectory = async (dir: string): Promise => { let entries = await pfs.readdir(dir); @@ -144,97 +144,103 @@ export class ExtensionManagementService extends Disposable implements IExtension return promise; }; - return collectFilesFromDirectory(extension.location.fsPath) - .then(files => files.map(f => ({ path: `extension/${path.relative(extension.location.fsPath, f)}`, localPath: f }))); - + const files = await collectFilesFromDirectory(extension.location.fsPath); + return files.map(f => ({ path: `extension/${path.relative(extension.location.fsPath, f)}`, localPath: f })); } - install(vsix: URI, isMachineScoped?: boolean): Promise { + async install(vsix: URI, isMachineScoped?: boolean): Promise { this.logService.trace('ExtensionManagementService#install', vsix.toString()); - return createCancelablePromise(token => { - return this.downloadVsix(vsix).then(downloadLocation => { - const zipPath = path.resolve(downloadLocation.fsPath); + return createCancelablePromise(async token => { - return getManifest(zipPath) - .then(manifest => { - const identifier = { id: getGalleryExtensionId(manifest.publisher, manifest.name) }; - let operation: InstallOperation = InstallOperation.Install; - if (manifest.engines && manifest.engines.vscode && !isEngineValid(manifest.engines.vscode, product.version)) { - return Promise.reject(new Error(nls.localize('incompatible', "Unable to install extension '{0}' as it is not compatible with VS Code '{1}'.", identifier.id, product.version))); - } - const identifierWithVersion = new ExtensionIdentifierWithVersion(identifier, manifest.version); - return this.getInstalled(ExtensionType.User) - .then(installedExtensions => { - const existing = installedExtensions.filter(i => areSameExtensions(identifier, i.identifier))[0]; - if (existing) { - isMachineScoped = isMachineScoped || existing.isMachineScoped; - operation = InstallOperation.Update; - if (identifierWithVersion.equals(new ExtensionIdentifierWithVersion(existing.identifier, existing.manifest.version))) { - return this.extensionsScanner.removeExtension(existing, 'existing').then(null, e => Promise.reject(new Error(nls.localize('restartCode', "Please restart VS Code before reinstalling {0}.", manifest.displayName || manifest.name)))); - } else if (semver.gt(existing.manifest.version, manifest.version)) { - return this.uninstallExtension(existing); - } - } else { - // Remove the extension with same version if it is already uninstalled. - // Installing a VSIX extension shall replace the existing extension always. - return this.unsetUninstalledAndGetLocal(identifierWithVersion) - .then(existing => { - if (existing) { - return this.extensionsScanner.removeExtension(existing, 'existing').then(null, e => Promise.reject(new Error(nls.localize('restartCode', "Please restart VS Code before reinstalling {0}.", manifest.displayName || manifest.name)))); - } - return undefined; - }); - } - return undefined; - }) - .then(() => { - this.logService.info('Installing the extension:', identifier.id); - this._onInstallExtension.fire({ identifier, zipPath }); - return this.getGalleryMetadata(getGalleryExtensionId(manifest.publisher, manifest.name)) - .then( - metadata => this.installFromZipPath(identifierWithVersion, zipPath, isMachineScoped ? { ...metadata, isMachineScoped } : metadata, operation, token), - () => this.installFromZipPath(identifierWithVersion, zipPath, isMachineScoped ? { isMachineScoped } : undefined, operation, token)) - .then( - local => { this.logService.info('Successfully installed the extension:', identifier.id); return local; }, - e => { - this.logService.error('Failed to install the extension:', identifier.id, e.message); - return Promise.reject(e); - }); - }); - }); - }); + const downloadLocation = await this.downloadVsix(vsix); + const zipPath = path.resolve(downloadLocation.fsPath); + + const manifest = await getManifest(zipPath); + const identifier = { id: getGalleryExtensionId(manifest.publisher, manifest.name) }; + let operation: InstallOperation = InstallOperation.Install; + if (manifest.engines && manifest.engines.vscode && !isEngineValid(manifest.engines.vscode, product.version)) { + throw new Error(nls.localize('incompatible', "Unable to install extension '{0}' as it is not compatible with VS Code '{1}'.", identifier.id, product.version)); + } + + const identifierWithVersion = new ExtensionIdentifierWithVersion(identifier, manifest.version); + const installedExtensions = await this.getInstalled(ExtensionType.User); + const existing = installedExtensions.find(i => areSameExtensions(identifier, i.identifier)); + if (existing) { + isMachineScoped = isMachineScoped || existing.isMachineScoped; + operation = InstallOperation.Update; + if (identifierWithVersion.equals(new ExtensionIdentifierWithVersion(existing.identifier, existing.manifest.version))) { + try { + await this.extensionsScanner.removeExtension(existing, 'existing'); + } catch (e) { + throw new Error(nls.localize('restartCode', "Please restart VS Code before reinstalling {0}.", manifest.displayName || manifest.name)); + } + } else if (semver.gt(existing.manifest.version, manifest.version)) { + await this.uninstallExtension(existing); + } + } else { + // Remove the extension with same version if it is already uninstalled. + // Installing a VSIX extension shall replace the existing extension always. + const existing = await this.unsetUninstalledAndGetLocal(identifierWithVersion); + if (existing) { + try { + await this.extensionsScanner.removeExtension(existing, 'existing'); + } catch (e) { + throw new Error(nls.localize('restartCode', "Please restart VS Code before reinstalling {0}.", manifest.displayName || manifest.name)); + } + } + } + + this.logService.info('Installing the extension:', identifier.id); + this._onInstallExtension.fire({ identifier, zipPath }); + + let metadata: IGalleryMetadata | undefined; + try { + metadata = await this.getGalleryMetadata(getGalleryExtensionId(manifest.publisher, manifest.name)); + } catch (e) { /* Ignore */ } + + try { + const local = await this.installFromZipPath(identifierWithVersion, zipPath, isMachineScoped ? { ...(metadata || {}), isMachineScoped } : metadata, operation, token); + this.logService.info('Successfully installed the extension:', identifier.id); + return local; + } catch (e) { + this.logService.error('Failed to install the extension:', identifier.id, e.message); + throw e; + } }); } - private downloadVsix(vsix: URI): Promise { + private async downloadVsix(vsix: URI): Promise { if (vsix.scheme === Schemas.file) { - return Promise.resolve(vsix); + return vsix; } if (!this.downloadService) { throw new Error('Download service is not available'); } - const downloadedLocation = path.join(tmpdir(), generateUuid()); - return this.downloadService.download(vsix, URI.file(downloadedLocation)).then(() => URI.file(downloadedLocation)); + + const downloadedLocation = URI.file(path.join(tmpdir(), generateUuid())); + await this.downloadService.download(vsix, downloadedLocation); + return downloadedLocation; } - private installFromZipPath(identifierWithVersion: ExtensionIdentifierWithVersion, zipPath: string, metadata: IMetadata | undefined, operation: InstallOperation, token: CancellationToken): Promise { - return this.toNonCancellablePromise(this.installExtension({ zipPath, identifierWithVersion, metadata }, token) - .then(local => this.installDependenciesAndPackExtensions(local, undefined) - .then( - () => local, - error => { - if (isNonEmptyArray(local.manifest.extensionDependencies)) { - this.logService.warn(`Cannot install dependencies of extension:`, local.identifier.id, error.message); - } - if (isNonEmptyArray(local.manifest.extensionPack)) { - this.logService.warn(`Cannot install packed extensions of extension:`, local.identifier.id, error.message); - } - return local; - })) - .then( - local => { this._onDidInstallExtension.fire({ identifier: identifierWithVersion.identifier, zipPath, local, operation }); return local; }, - error => { this._onDidInstallExtension.fire({ identifier: identifierWithVersion.identifier, zipPath, operation, error }); return Promise.reject(error); } - )); + private async installFromZipPath(identifierWithVersion: ExtensionIdentifierWithVersion, zipPath: string, metadata: IMetadata | undefined, operation: InstallOperation, token: CancellationToken): Promise { + try { + const local = await this.installExtension({ zipPath, identifierWithVersion, metadata }, token); + try { + await this.installDependenciesAndPackExtensions(local, undefined); + } catch (error) { + if (isNonEmptyArray(local.manifest.extensionDependencies)) { + this.logService.warn(`Cannot install dependencies of extension:`, local.identifier.id, error.message); + } + if (isNonEmptyArray(local.manifest.extensionPack)) { + this.logService.warn(`Cannot install packed extensions of extension:`, local.identifier.id, error.message); + } + } + this._onDidInstallExtension.fire({ identifier: identifierWithVersion.identifier, zipPath, local, operation }); + return local; + } catch (error) { + this._onDidInstallExtension.fire({ identifier: identifierWithVersion.identifier, zipPath, operation, error }); + throw error; + } } async canInstall(extension: IGalleryExtension): Promise { @@ -243,17 +249,68 @@ export class ExtensionManagementService extends Disposable implements IExtension async installFromGallery(extension: IGalleryExtension, isMachineScoped?: boolean): Promise { if (!this.galleryService.isEnabled()) { - return Promise.reject(new Error(nls.localize('MarketPlaceDisabled', "Marketplace is not enabled"))); + throw new Error(nls.localize('MarketPlaceDisabled', "Marketplace is not enabled")); } - const startTime = new Date().getTime(); - const onDidInstallExtensionSuccess = (extension: IGalleryExtension, operation: InstallOperation, local: ILocalExtension) => { + try { + extension = await this.checkAndGetCompatibleVersion(extension); + } catch (error) { + const errorCode = error && (error).code ? (error).code : ERROR_UNKNOWN; + this.logService.error(`Failed to install extension:`, extension.identifier.id, error ? error.message : errorCode); + this.reportTelemetry(this.getTelemetryEvent(InstallOperation.Install), getGalleryExtensionTelemetryData(extension), undefined, error); + if (error instanceof Error) { + error.name = errorCode; + } + throw error; + } + + const key = new ExtensionIdentifierWithVersion(extension.identifier, extension.version).key(); + let cancellablePromise = this.installingExtensions.get(key); + if (!cancellablePromise) { + cancellablePromise = createCancelablePromise(token => this.doInstallFromGallery(extension, !!isMachineScoped, token)); + this.installingExtensions.set(key, cancellablePromise); + cancellablePromise.finally(() => this.installingExtensions.delete(key)); + } + + return cancellablePromise; + } + + private async doInstallFromGallery(extension: IGalleryExtension, isMachineScoped: boolean, token: CancellationToken): Promise { + const startTime = new Date().getTime(); + let operation: InstallOperation = InstallOperation.Install; + this.logService.info('Installing extension:', extension.identifier.id); + this._onInstallExtension.fire({ identifier: extension.identifier, gallery: extension }); + + try { + const installed = await this.getInstalled(ExtensionType.User); + const existingExtension = installed.find(i => areSameExtensions(i.identifier, extension.identifier)); + if (existingExtension) { + operation = InstallOperation.Update; + } + + const installableExtension = await this.downloadInstallableExtension(extension, operation); + installableExtension.metadata.isMachineScoped = isMachineScoped || existingExtension?.isMachineScoped; + const local = await this.installExtension(installableExtension, token); + + try { await this.extensionsDownloader.delete(URI.file(installableExtension.zipPath)); } catch (error) { /* Ignore */ } + + try { + await this.installDependenciesAndPackExtensions(local, existingExtension); + } catch (error) { + try { await this.uninstall(local); } catch (error) { /* Ignore */ } + throw error; + } + + if (existingExtension && semver.neq(existingExtension.manifest.version, extension.version)) { + await this.setUninstalled(existingExtension); + } + this.logService.info(`Extensions installed successfully:`, extension.identifier.id); this._onDidInstallExtension.fire({ identifier: extension.identifier, gallery: extension, local, operation }); this.reportTelemetry(this.getTelemetryEvent(operation), getGalleryExtensionTelemetryData(extension), new Date().getTime() - startTime, undefined); - }; + return local; - const onDidInstallExtensionFailure = (extension: IGalleryExtension, operation: InstallOperation, error: Error) => { + } catch (error) { const errorCode = error && (error).code ? (error).code : ERROR_UNKNOWN; this.logService.error(`Failed to install extension:`, extension.identifier.id, error ? error.message : errorCode); this._onDidInstallExtension.fire({ identifier: extension.identifier, gallery: extension, operation, error: errorCode }); @@ -261,162 +318,107 @@ export class ExtensionManagementService extends Disposable implements IExtension if (error instanceof Error) { error.name = errorCode; } - }; - - try { - extension = await this.checkAndGetCompatibleVersion(extension); - } catch (error) { - onDidInstallExtensionFailure(extension, InstallOperation.Install, error); - return Promise.reject(error); + throw error; } - - const key = new ExtensionIdentifierWithVersion(extension.identifier, extension.version).key(); - let cancellablePromise = this.installingExtensions.get(key); - if (!cancellablePromise) { - - this.logService.info('Installing extension:', extension.identifier.id); - this._onInstallExtension.fire({ identifier: extension.identifier, gallery: extension }); - - let operation: InstallOperation = InstallOperation.Install; - let cancellationToken: CancellationToken, successCallback: (local: ILocalExtension) => void, errorCallback: (e?: any) => any | null; - cancellablePromise = createCancelablePromise(token => { cancellationToken = token; return new Promise((c, e) => { successCallback = c; errorCallback = e; }); }); - this.installingExtensions.set(key, cancellablePromise); - try { - const installed = await this.getInstalled(ExtensionType.User); - const existingExtension = installed.find(i => areSameExtensions(i.identifier, extension.identifier)); - if (existingExtension) { - operation = InstallOperation.Update; - } - - this.downloadInstallableExtension(extension, operation) - .then(installableExtension => { - installableExtension.metadata.isMachineScoped = isMachineScoped || existingExtension?.isMachineScoped; - return this.installExtension(installableExtension, cancellationToken) - .then(local => this.extensionsDownloader.delete(URI.file(installableExtension.zipPath)).finally(() => { }).then(() => local)); - }) - .then(local => this.installDependenciesAndPackExtensions(local, existingExtension) - .then(() => local, error => this.uninstall(local).then(() => Promise.reject(error), () => Promise.reject(error)))) - .then( - async local => { - if (existingExtension && semver.neq(existingExtension.manifest.version, extension.version)) { - await this.setUninstalled(existingExtension); - } - this.installingExtensions.delete(key); - onDidInstallExtensionSuccess(extension, operation, local); - successCallback(local); - }, - error => { - this.installingExtensions.delete(key); - onDidInstallExtensionFailure(extension, operation, error); - errorCallback(error); - }); - - } catch (error) { - this.installingExtensions.delete(key); - onDidInstallExtensionFailure(extension, operation, error); - return Promise.reject(error); - } - - } - - return cancellablePromise; } private async checkAndGetCompatibleVersion(extension: IGalleryExtension): Promise { if (await this.isMalicious(extension)) { - return Promise.reject(new ExtensionManagementError(nls.localize('malicious extension', "Can't install extension since it was reported to be problematic."), INSTALL_ERROR_MALICIOUS)); + throw new ExtensionManagementError(nls.localize('malicious extension', "Can't install extension since it was reported to be problematic."), INSTALL_ERROR_MALICIOUS); } const compatibleExtension = await this.galleryService.getCompatibleExtension(extension); - if (!compatibleExtension) { - return Promise.reject(new ExtensionManagementError(nls.localize('notFoundCompatibleDependency', "Unable to install '{0}' extension because it is not compatible with the current version of VS Code (version {1}).", extension.identifier.id, product.version), INSTALL_ERROR_INCOMPATIBLE)); + throw new ExtensionManagementError(nls.localize('notFoundCompatibleDependency', "Unable to install '{0}' extension because it is not compatible with the current version of VS Code (version {1}).", extension.identifier.id, product.version), INSTALL_ERROR_INCOMPATIBLE); } return compatibleExtension; } - reinstallFromGallery(extension: ILocalExtension): Promise { + async reinstallFromGallery(extension: ILocalExtension): Promise { this.logService.trace('ExtensionManagementService#reinstallFromGallery', extension.identifier.id); if (!this.galleryService.isEnabled()) { - return Promise.reject(new Error(nls.localize('MarketPlaceDisabled', "Marketplace is not enabled"))); + throw new Error(nls.localize('MarketPlaceDisabled', "Marketplace is not enabled")); } - return this.findGalleryExtension(extension) - .then(galleryExtension => { - if (galleryExtension) { - return this.setUninstalled(extension) - .then(() => this.extensionsScanner.removeUninstalledExtension(extension) - .then( - () => this.installFromGallery(galleryExtension).then(), - e => Promise.reject(new Error(nls.localize('removeError', "Error while removing the extension: {0}. Please Quit and Start VS Code before trying again.", toErrorMessage(e)))))); - } - return Promise.reject(new Error(nls.localize('Not a Marketplace extension', "Only Marketplace Extensions can be reinstalled"))); - }); + + const galleryExtension = await this.findGalleryExtension(extension); + if (!galleryExtension) { + throw new Error(nls.localize('Not a Marketplace extension', "Only Marketplace Extensions can be reinstalled")); + } + + await this.setUninstalled(extension); + try { + await this.extensionsScanner.removeUninstalledExtension(extension); + } catch (e) { + throw new Error(nls.localize('removeError', "Error while removing the extension: {0}. Please Quit and Start VS Code before trying again.", toErrorMessage(e))); + } + + await this.installFromGallery(galleryExtension); } private getTelemetryEvent(operation: InstallOperation): string { return operation === InstallOperation.Update ? 'extensionGallery:update' : 'extensionGallery:install'; } - private isMalicious(extension: IGalleryExtension): Promise { - return this.getExtensionsReport() - .then(report => getMaliciousExtensionsSet(report).has(extension.identifier.id)); + private async isMalicious(extension: IGalleryExtension): Promise { + const report = await this.getExtensionsReport(); + return getMaliciousExtensionsSet(report).has(extension.identifier.id); } - private downloadInstallableExtension(extension: IGalleryExtension, operation: InstallOperation): Promise> { + private async downloadInstallableExtension(extension: IGalleryExtension, operation: InstallOperation): Promise> { const metadata = { id: extension.identifier.uuid, publisherId: extension.publisherId, publisherDisplayName: extension.publisherDisplayName, }; - this.logService.trace('Started downloading extension:', extension.identifier.id); - return this.extensionsDownloader.downloadExtension(extension, operation) - .then( - zip => { - const zipPath = zip.fsPath; - this.logService.info('Downloaded extension:', extension.identifier.id, zipPath); - return getManifest(zipPath) - .then( - manifest => (>{ zipPath, identifierWithVersion: new ExtensionIdentifierWithVersion(extension.identifier, manifest.version), metadata }), - error => Promise.reject(new ExtensionManagementError(this.joinErrors(error).message, INSTALL_ERROR_VALIDATING)) - ); - }, - error => Promise.reject(new ExtensionManagementError(this.joinErrors(error).message, INSTALL_ERROR_DOWNLOADING))); + let zipPath; + try { + this.logService.trace('Started downloading extension:', extension.identifier.id); + const zip = await this.extensionsDownloader.downloadExtension(extension, operation); + this.logService.info('Downloaded extension:', extension.identifier.id, zipPath); + zipPath = zip.fsPath; + } catch (error) { + throw new ExtensionManagementError(this.joinErrors(error).message, INSTALL_ERROR_DOWNLOADING); + } + + try { + const manifest = await getManifest(zipPath); + return (>{ zipPath, identifierWithVersion: new ExtensionIdentifierWithVersion(extension.identifier, manifest.version), metadata }); + } catch (error) { + throw new ExtensionManagementError(this.joinErrors(error).message, INSTALL_ERROR_VALIDATING); + } } - private installExtension(installableExtension: InstallableExtension, token: CancellationToken): Promise { - return this.unsetUninstalledAndGetLocal(installableExtension.identifierWithVersion) - .then( - local => { - if (local) { - return local; - } - return this.extractAndInstall(installableExtension, token); - }, - e => { - if (isMacintosh) { - return Promise.reject(new ExtensionManagementError(nls.localize('quitCode', "Unable to install the extension. Please Quit and Start VS Code before reinstalling."), INSTALL_ERROR_UNSET_UNINSTALLED)); - } - return Promise.reject(new ExtensionManagementError(nls.localize('exitCode', "Unable to install the extension. Please Exit and Start VS Code before reinstalling."), INSTALL_ERROR_UNSET_UNINSTALLED)); - }); + private async installExtension(installableExtension: InstallableExtension, token: CancellationToken): Promise { + try { + const local = await this.unsetUninstalledAndGetLocal(installableExtension.identifierWithVersion); + if (local) { + return local; + } + } catch (e) { + if (isMacintosh) { + throw new ExtensionManagementError(nls.localize('quitCode', "Unable to install the extension. Please Quit and Start VS Code before reinstalling."), INSTALL_ERROR_UNSET_UNINSTALLED); + } else { + throw new ExtensionManagementError(nls.localize('exitCode', "Unable to install the extension. Please Exit and Start VS Code before reinstalling."), INSTALL_ERROR_UNSET_UNINSTALLED); + } + } + return this.extractAndInstall(installableExtension, token); } - private unsetUninstalledAndGetLocal(identifierWithVersion: ExtensionIdentifierWithVersion): Promise { - return this.isUninstalled(identifierWithVersion) - .then(isUninstalled => { - if (isUninstalled) { - this.logService.trace('Removing the extension from uninstalled list:', identifierWithVersion.identifier.id); - // If the same version of extension is marked as uninstalled, remove it from there and return the local. - return this.unsetUninstalled(identifierWithVersion) - .then(() => { - this.logService.info('Removed the extension from uninstalled list:', identifierWithVersion.identifier.id); - return this.getInstalled(ExtensionType.User); - }) - .then(installed => installed.filter(i => new ExtensionIdentifierWithVersion(i.identifier, i.manifest.version).equals(identifierWithVersion))[0]); - } - return null; - }); + private async unsetUninstalledAndGetLocal(identifierWithVersion: ExtensionIdentifierWithVersion): Promise { + const isUninstalled = await this.isUninstalled(identifierWithVersion); + if (!isUninstalled) { + return null; + } + + this.logService.trace('Removing the extension from uninstalled list:', identifierWithVersion.identifier.id); + // If the same version of extension is marked as uninstalled, remove it from there and return the local. + await this.unsetUninstalled(identifierWithVersion); + this.logService.info('Removed the extension from uninstalled list:', identifierWithVersion.identifier.id); + + const installed = await this.getInstalled(ExtensionType.User); + return installed.find(i => new ExtensionIdentifierWithVersion(i.identifier, i.manifest.version).equals(identifierWithVersion)) || null; } private async extractAndInstall({ zipPath, identifierWithVersion, metadata }: InstallableExtension, token: CancellationToken): Promise { @@ -430,57 +432,56 @@ export class ExtensionManagementService extends Disposable implements IExtension } private async installDependenciesAndPackExtensions(installed: ILocalExtension, existing: ILocalExtension | undefined): Promise { - if (this.galleryService.isEnabled()) { - const dependenciesAndPackExtensions: string[] = installed.manifest.extensionDependencies || []; - if (installed.manifest.extensionPack) { - for (const extension of installed.manifest.extensionPack) { - // add only those extensions which are new in currently installed extension - if (!(existing && existing.manifest.extensionPack && existing.manifest.extensionPack.some(old => areSameExtensions({ id: old }, { id: extension })))) { - if (dependenciesAndPackExtensions.every(e => !areSameExtensions({ id: e }, { id: extension }))) { - dependenciesAndPackExtensions.push(extension); - } + if (!this.galleryService.isEnabled()) { + return; + } + const dependenciesAndPackExtensions: string[] = installed.manifest.extensionDependencies || []; + if (installed.manifest.extensionPack) { + for (const extension of installed.manifest.extensionPack) { + // add only those extensions which are new in currently installed extension + if (!(existing && existing.manifest.extensionPack && existing.manifest.extensionPack.some(old => areSameExtensions({ id: old }, { id: extension })))) { + if (dependenciesAndPackExtensions.every(e => !areSameExtensions({ id: e }, { id: extension }))) { + dependenciesAndPackExtensions.push(extension); } } } - if (dependenciesAndPackExtensions.length) { - return this.getInstalled() - .then(installed => { - // filter out installed extensions - const names = dependenciesAndPackExtensions.filter(id => installed.every(({ identifier: galleryIdentifier }) => !areSameExtensions(galleryIdentifier, { id }))); - if (names.length) { - return this.galleryService.query({ names, pageSize: dependenciesAndPackExtensions.length }, CancellationToken.None) - .then(galleryResult => { - const extensionsToInstall = galleryResult.firstPage; - return Promise.all(extensionsToInstall.map(e => this.installFromGallery(e))) - .then(undefined, errors => this.rollback(extensionsToInstall).then(() => Promise.reject(errors), () => Promise.reject(errors))); - }); - } - return; - }); + } + if (dependenciesAndPackExtensions.length) { + const installed = await this.getInstalled(); + // filter out installed extensions + const names = dependenciesAndPackExtensions.filter(id => installed.every(({ identifier: galleryIdentifier }) => !areSameExtensions(galleryIdentifier, { id }))); + if (names.length) { + const galleryResult = await this.galleryService.query({ names, pageSize: dependenciesAndPackExtensions.length }, CancellationToken.None); + const extensionsToInstall = galleryResult.firstPage; + try { + await Promise.all(extensionsToInstall.map(e => this.installFromGallery(e))); + } catch (error) { + try { await this.rollback(extensionsToInstall); } catch (e) { /* ignore */ } + throw error; + } } } - return Promise.resolve(undefined); } - private rollback(extensions: IGalleryExtension[]): Promise { - return this.getInstalled(ExtensionType.User) - .then(installed => - Promise.all(installed.filter(local => extensions.some(galleryExtension => new ExtensionIdentifierWithVersion(local.identifier, local.manifest.version).equals(new ExtensionIdentifierWithVersion(galleryExtension.identifier, galleryExtension.version)))) // Check with version because we want to rollback the exact version - .map(local => this.uninstall(local)))) - .then(() => undefined, () => undefined); + private async rollback(extensions: IGalleryExtension[]): Promise { + const installed = await this.getInstalled(ExtensionType.User); + const extensionsToUninstall = installed.filter(local => extensions.some(galleryExtension => new ExtensionIdentifierWithVersion(local.identifier, local.manifest.version).equals(new ExtensionIdentifierWithVersion(galleryExtension.identifier, galleryExtension.version)))); // Check with version because we want to rollback the exact version + await Promise.all(extensionsToUninstall.map(local => this.uninstall(local))); } - uninstall(extension: ILocalExtension): Promise { + async uninstall(extension: ILocalExtension): Promise { this.logService.trace('ExtensionManagementService#uninstall', extension.identifier.id); - return this.toNonCancellablePromise(this.getInstalled(ExtensionType.User) - .then(installed => { - const extensionToUninstall = installed.filter(e => areSameExtensions(e.identifier, extension.identifier))[0]; - if (extensionToUninstall) { - return this.checkForDependenciesAndUninstall(extensionToUninstall, installed).then(undefined, error => Promise.reject(this.joinErrors(error))); - } else { - return Promise.reject(new Error(nls.localize('notInstalled', "Extension '{0}' is not installed.", extension.manifest.displayName || extension.manifest.name))); - } - })); + const installed = await this.getInstalled(ExtensionType.User); + const extensionToUninstall = installed.find(e => areSameExtensions(e.identifier, extension.identifier)); + if (!extensionToUninstall) { + throw new Error(nls.localize('notInstalled', "Extension '{0}' is not installed.", extension.manifest.displayName || extension.manifest.name)); + } + + try { + await this.checkForDependenciesAndUninstall(extensionToUninstall, installed); + } catch (error) { + throw this.joinErrors(error); + } } async updateMetadata(local: ILocalExtension, metadata: IGalleryMetadata): Promise { @@ -490,25 +491,27 @@ export class ExtensionManagementService extends Disposable implements IExtension return local; } - private getGalleryMetadata(extensionName: string): Promise { - return this.findGalleryExtensionByName(extensionName) - .then(galleryExtension => galleryExtension ? { id: galleryExtension.identifier.uuid, publisherDisplayName: galleryExtension.publisherDisplayName, publisherId: galleryExtension.publisherId } : undefined); + private async getGalleryMetadata(extensionName: string): Promise { + const galleryExtension = await this.findGalleryExtensionByName(extensionName); + return galleryExtension ? { id: galleryExtension.identifier.uuid, publisherDisplayName: galleryExtension.publisherDisplayName, publisherId: galleryExtension.publisherId } : undefined; } - private findGalleryExtension(local: ILocalExtension): Promise { + private async findGalleryExtension(local: ILocalExtension): Promise { if (local.identifier.uuid) { - return this.findGalleryExtensionById(local.identifier.uuid) - .then(galleryExtension => galleryExtension ? galleryExtension : this.findGalleryExtensionByName(local.identifier.id)); + const galleryExtension = await this.findGalleryExtensionById(local.identifier.uuid); + return galleryExtension ? galleryExtension : this.findGalleryExtensionByName(local.identifier.id); } return this.findGalleryExtensionByName(local.identifier.id); } - private findGalleryExtensionById(uuid: string): Promise { - return this.galleryService.query({ ids: [uuid], pageSize: 1 }, CancellationToken.None).then(galleryResult => galleryResult.firstPage[0]); + private async findGalleryExtensionById(uuid: string): Promise { + const galleryResult = await this.galleryService.query({ ids: [uuid], pageSize: 1 }, CancellationToken.None); + return galleryResult.firstPage[0]; } - private findGalleryExtensionByName(name: string): Promise { - return this.galleryService.query({ names: [name], pageSize: 1 }, CancellationToken.None).then(galleryResult => galleryResult.firstPage[0]); + private async findGalleryExtensionByName(name: string): Promise { + const galleryResult = await this.galleryService.query({ names: [name], pageSize: 1 }, CancellationToken.None); + return galleryResult.firstPage[0]; } private joinErrors(errorOrErrors: (Error | string) | (Array)): Error { @@ -521,20 +524,20 @@ export class ExtensionManagementService extends Disposable implements IExtension }, new Error('')); } - private checkForDependenciesAndUninstall(extension: ILocalExtension, installed: ILocalExtension[]): Promise { - return this.preUninstallExtension(extension) - .then(() => { - const packedExtensions = this.getAllPackExtensionsToUninstall(extension, installed); - if (packedExtensions.length) { - return this.uninstallExtensions(extension, packedExtensions, installed); - } - return this.uninstallExtensions(extension, [], installed); - }) - .then(() => this.postUninstallExtension(extension), - error => { - this.postUninstallExtension(extension, new ExtensionManagementError(error instanceof Error ? error.message : error, INSTALL_ERROR_LOCAL)); - return Promise.reject(error); - }); + private async checkForDependenciesAndUninstall(extension: ILocalExtension, installed: ILocalExtension[]): Promise { + try { + await this.preUninstallExtension(extension); + const packedExtensions = this.getAllPackExtensionsToUninstall(extension, installed); + if (packedExtensions.length) { + await this.uninstallExtensions(extension, packedExtensions, installed); + } else { + await this.uninstallExtensions(extension, [], installed); + } + } catch (error) { + await this.postUninstallExtension(extension, new ExtensionManagementError(error instanceof Error ? error.message : error, INSTALL_ERROR_LOCAL)); + throw error; + } + await this.postUninstallExtension(extension); } private async uninstallExtensions(extension: ILocalExtension, otherExtensionsToUninstall: ILocalExtension[], installed: ILocalExtension[]): Promise { @@ -605,33 +608,36 @@ export class ExtensionManagementService extends Disposable implements IExtension return installed.filter(e => e.manifest.extensionDependencies && e.manifest.extensionDependencies.some(id => areSameExtensions({ id }, extension.identifier))); } - private doUninstall(extension: ILocalExtension): Promise { - return this.preUninstallExtension(extension) - .then(() => this.uninstallExtension(extension)) - .then(() => this.postUninstallExtension(extension), - error => { - this.postUninstallExtension(extension, new ExtensionManagementError(error instanceof Error ? error.message : error, INSTALL_ERROR_LOCAL)); - return Promise.reject(error); - }); + private async doUninstall(extension: ILocalExtension): Promise { + try { + await this.preUninstallExtension(extension); + await this.uninstallExtension(extension); + } catch (error) { + await this.postUninstallExtension(extension, new ExtensionManagementError(error instanceof Error ? error.message : error, INSTALL_ERROR_LOCAL)); + throw error; + } + await this.postUninstallExtension(extension); } - private preUninstallExtension(extension: ILocalExtension): Promise { - return Promise.resolve(pfs.exists(extension.location.fsPath)) - .then(exists => exists ? null : Promise.reject(new Error(nls.localize('notExists', "Could not find extension")))) - .then(() => { - this.logService.info('Uninstalling extension:', extension.identifier.id); - this._onUninstallExtension.fire(extension.identifier); - }); + private async preUninstallExtension(extension: ILocalExtension): Promise { + const exists = await pfs.exists(extension.location.fsPath); + if (!exists) { + throw new Error(nls.localize('notExists', "Could not find extension")); + } + this.logService.info('Uninstalling extension:', extension.identifier.id); + this._onUninstallExtension.fire(extension.identifier); } - private uninstallExtension(local: ILocalExtension): Promise { + private async uninstallExtension(local: ILocalExtension): Promise { let promise = this.uninstallingExtensions.get(local.identifier.id); if (!promise) { // Set all versions of the extension as uninstalled - promise = createCancelablePromise(token => this.extensionsScanner.scanUserExtensions(false) - .then(userExtensions => this.setUninstalled(...userExtensions.filter(u => areSameExtensions(u.identifier, local.identifier)))) - .then(() => { this.uninstallingExtensions.delete(local.identifier.id); })); + promise = createCancelablePromise(async () => { + const userExtensions = await this.extensionsScanner.scanUserExtensions(false); + await this.setUninstalled(...userExtensions.filter(u => areSameExtensions(u.identifier, local.identifier))); + }); this.uninstallingExtensions.set(local.identifier.id, promise); + promise.finally(() => this.uninstallingExtensions.delete(local.identifier.id)); } return promise; } @@ -643,7 +649,9 @@ export class ExtensionManagementService extends Disposable implements IExtension this.logService.info('Successfully uninstalled extension:', extension.identifier.id); // only report if extension has a mapped gallery extension. UUID identifies the gallery extension. if (extension.identifier.uuid) { - await this.galleryService.reportStatistic(extension.manifest.publisher, extension.manifest.name, extension.manifest.version, StatisticType.Uninstall); + try { + await this.galleryService.reportStatistic(extension.manifest.publisher, extension.manifest.name, extension.manifest.version, StatisticType.Uninstall); + } catch (error) { /* ignore */ } } } this.reportTelemetry('extensionGallery:uninstall', getLocalExtensionTelemetryData(extension), undefined, error); @@ -659,8 +667,9 @@ export class ExtensionManagementService extends Disposable implements IExtension return this.extensionsScanner.cleanUp(); } - private isUninstalled(identifier: ExtensionIdentifierWithVersion): Promise { - return this.filterUninstalled(identifier).then(uninstalled => uninstalled.length === 1); + private async isUninstalled(identifier: ExtensionIdentifierWithVersion): Promise { + const uninstalled = await this.filterUninstalled(identifier); + return uninstalled.length === 1; } private filterUninstalled(...identifiers: ExtensionIdentifierWithVersion[]): Promise { @@ -678,8 +687,8 @@ export class ExtensionManagementService extends Disposable implements IExtension private setUninstalled(...extensions: ILocalExtension[]): Promise<{ [id: string]: boolean }> { const ids: ExtensionIdentifierWithVersion[] = extensions.map(e => new ExtensionIdentifierWithVersion(e.identifier, e.manifest.version)); return this.extensionsScanner.withUninstalledExtensions(uninstalled => { - const newUninstalled = (ids.reduce>((result, id) => { result[id.key()] = true; return result; }, {})); - return { ...uninstalled, ...newUninstalled }; + ids.forEach(id => uninstalled[id.key()] = true); + return uninstalled; }); } @@ -698,21 +707,16 @@ export class ExtensionManagementService extends Disposable implements IExtension return this.reportedExtensions; } - private updateReportCache(): Promise { - this.logService.trace('ExtensionManagementService.refreshReportedCache'); - - return this.galleryService.getExtensionsReport() - .then(result => { - this.logService.trace(`ExtensionManagementService.refreshReportedCache - got ${result.length} reported extensions from service`); - return result; - }, err => { - this.logService.trace('ExtensionManagementService.refreshReportedCache - failed to get extension report'); - return []; - }); - } - - private toNonCancellablePromise(promise: Promise): Promise { - return new Promise((c, e) => promise.then(result => c(result), error => e(error))); + private async updateReportCache(): Promise { + try { + this.logService.trace('ExtensionManagementService.refreshReportedCache'); + const result = await this.galleryService.getExtensionsReport(); + this.logService.trace(`ExtensionManagementService.refreshReportedCache - got ${result.length} reported extensions from service`); + return result; + } catch (err) { + this.logService.trace('ExtensionManagementService.refreshReportedCache - failed to get extension report'); + return []; + } } private reportTelemetry(eventName: string, extensionData: any, duration?: number, error?: Error): void { diff --git a/src/vs/platform/extensionManagement/node/extensionsScanner.ts b/src/vs/platform/extensionManagement/node/extensionsScanner.ts index 819dc9d2bd2..40218225f3e 100644 --- a/src/vs/platform/extensionManagement/node/extensionsScanner.ts +++ b/src/vs/platform/extensionManagement/node/extensionsScanner.ts @@ -69,7 +69,12 @@ export class ExtensionsScanner extends Disposable { promises.push(this.scanUserExtensions(true).then(null, e => Promise.reject(new ExtensionManagementError(this.joinErrors(e).message, ERROR_SCANNING_USER_EXTENSIONS)))); } - return Promise.all(promises).then(flatten, errors => Promise.reject(this.joinErrors(errors))); + try { + const result = await Promise.all(promises); + return flatten(result); + } catch (error) { + throw this.joinErrors(error); + } } async scanUserExtensions(excludeOutdated: boolean): Promise { @@ -145,20 +150,31 @@ export class ExtensionsScanner extends Disposable { async withUninstalledExtensions(fn: (uninstalled: IStringDictionary) => T): Promise { return this.uninstalledFileLimiter.queue(async () => { - let result: T | null = null; - return pfs.readFile(this.uninstalledPath, 'utf8') - .then(undefined, err => err.code === 'ENOENT' ? Promise.resolve('{}') : Promise.reject(err)) - .then<{ [id: string]: boolean }>(raw => { try { return JSON.parse(raw); } catch (e) { return {}; } }) - .then(uninstalled => { result = fn(uninstalled); return uninstalled; }) - .then(uninstalled => { - if (Object.keys(uninstalled).length === 0) { - return pfs.rimraf(this.uninstalledPath); - } else { - const raw = JSON.stringify(uninstalled); - return pfs.writeFile(this.uninstalledPath, raw); - } - }) - .then(() => result); + let raw: string | undefined; + try { + raw = await pfs.readFile(this.uninstalledPath, 'utf8'); + } catch (err) { + if (err.code !== 'ENOENT') { + throw err; + } + } + + let uninstalled = {}; + if (raw) { + try { + uninstalled = JSON.parse(raw); + } catch (e) { /* ignore */ } + } + + const result = fn(uninstalled); + + if (Object.keys(uninstalled).length) { + await pfs.writeFile(this.uninstalledPath, JSON.stringify(uninstalled)); + } else { + await pfs.rimraf(this.uninstalledPath); + } + + return result; }); } @@ -173,27 +189,35 @@ export class ExtensionsScanner extends Disposable { await this.withUninstalledExtensions(uninstalled => delete uninstalled[new ExtensionIdentifierWithVersion(extension.identifier, extension.manifest.version).key()]); } - private extractAtLocation(identifier: IExtensionIdentifier, zipPath: string, location: string, token: CancellationToken): Promise { + private async extractAtLocation(identifier: IExtensionIdentifier, zipPath: string, location: string, token: CancellationToken): Promise { this.logService.trace(`Started extracting the extension from ${zipPath} to ${location}`); - return pfs.rimraf(location) - .then( - () => extract(zipPath, location, { sourcePath: 'extension', overwrite: true }, token) - .then( - () => this.logService.info(`Extracted extension to ${location}:`, identifier.id), - e => pfs.rimraf(location).finally(() => { }) - .then(() => Promise.reject(new ExtensionManagementError(e.message, e instanceof ExtractError && e.type ? e.type : INSTALL_ERROR_EXTRACTING)))), - e => Promise.reject(new ExtensionManagementError(this.joinErrors(e).message, INSTALL_ERROR_DELETING))); + + // Clean the location + try { + await pfs.rimraf(location); + } catch (e) { + throw new ExtensionManagementError(this.joinErrors(e).message, INSTALL_ERROR_DELETING); + } + + try { + await extract(zipPath, location, { sourcePath: 'extension', overwrite: true }, token); + this.logService.info(`Extracted extension to ${location}:`, identifier.id); + } catch (e) { + try { await pfs.rimraf(location); } catch (e) { /* Ignore */ } + throw new ExtensionManagementError(e.message, e instanceof ExtractError && e.type ? e.type : INSTALL_ERROR_EXTRACTING); + } } - private rename(identifier: IExtensionIdentifier, extractPath: string, renamePath: string, retryUntil: number): Promise { - return pfs.rename(extractPath, renamePath) - .then(undefined, error => { - if (isWindows && error && error.code === 'EPERM' && Date.now() < retryUntil) { - this.logService.info(`Failed renaming ${extractPath} to ${renamePath} with 'EPERM' error. Trying again...`, identifier.id); - return this.rename(identifier, extractPath, renamePath, retryUntil); - } - return Promise.reject(new ExtensionManagementError(error.message || localize('renameError', "Unknown error while renaming {0} to {1}", extractPath, renamePath), error.code || INSTALL_ERROR_RENAMING)); - }); + private async rename(identifier: IExtensionIdentifier, extractPath: string, renamePath: string, retryUntil: number): Promise { + try { + await pfs.rename(extractPath, renamePath); + } catch (error) { + if (isWindows && error && error.code === 'EPERM' && Date.now() < retryUntil) { + this.logService.info(`Failed renaming ${extractPath} to ${renamePath} with 'EPERM' error. Trying again...`, identifier.id); + return this.rename(identifier, extractPath, renamePath, retryUntil); + } + throw new ExtensionManagementError(error.message || localize('renameError', "Unknown error while renaming {0} to {1}", extractPath, renamePath), error.code || INSTALL_ERROR_RENAMING); + } } private async scanSystemExtensions(): Promise { diff --git a/src/vs/platform/launch/electron-main/launchMainService.ts b/src/vs/platform/launch/electron-main/launchMainService.ts index 7ebd8132de1..67a8d7f5345 100644 --- a/src/vs/platform/launch/electron-main/launchMainService.ts +++ b/src/vs/platform/launch/electron-main/launchMainService.ts @@ -230,7 +230,7 @@ export class LaunchMainService implements ILaunchMainService { getRemoteDiagnostics(options: IRemoteDiagnosticOptions): Promise<(IRemoteDiagnosticInfo | IRemoteDiagnosticError)[]> { const windows = this.windowsMainService.getWindows(); const promises: Promise[] = windows.map(window => { - return new Promise((resolve, reject) => { + return new Promise((resolve) => { const remoteAuthority = window.remoteAuthority; if (remoteAuthority) { const replyChannel = `vscode:getDiagnosticInfoResponse${window.id}`; @@ -254,7 +254,7 @@ export class LaunchMainService implements ILaunchMainService { resolve({ hostName: remoteAuthority, errorMessage: `Fetching remote diagnostics for '${remoteAuthority}' timed out.` }); }, 5000); } else { - resolve(); + resolve(undefined); } }); }); diff --git a/src/vs/vscode.d.ts b/src/vs/vscode.d.ts index 680d2921f0f..5eef883dee0 100644 --- a/src/vs/vscode.d.ts +++ b/src/vs/vscode.d.ts @@ -3820,6 +3820,13 @@ declare module 'vscode' { * A string that should be used when comparing this item * with other items. When `falsy` the [label](#CompletionItem.label) * is used. + * + * Note that `sortText` is only used for the initial ordering of completion + * items. When having a leading word (prefix) ordering is based on how + * well completion match that prefix and the initial ordering is only used + * when completions match equal. The prefix is defined by the + * [`range`](#CompletionItem.range)-property and can therefore be different + * for each completion. */ sortText?: string; @@ -3827,6 +3834,10 @@ declare module 'vscode' { * A string that should be used when filtering a set of * completion items. When `falsy` the [label](#CompletionItem.label) * is used. + * + * Note that the filter text is matched against the leading word (prefix) which is defined + * by the [`range`](#CompletionItem.range)-property. + * prefix. */ filterText?: string; diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index b732e88f1ec..108a6515e42 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -1350,17 +1350,24 @@ declare module 'vscode' { } export interface WorkspaceEdit { + replaceNotebookMetadata(uri: Uri, value: NotebookDocumentMetadata): void; replaceCells(uri: Uri, start: number, end: number, cells: NotebookCellData[], metadata?: WorkspaceEditEntryMetadata): void; replaceCellOutput(uri: Uri, index: number, outputs: CellOutput[], metadata?: WorkspaceEditEntryMetadata): void; replaceCellMetadata(uri: Uri, index: number, cellMetadata: NotebookCellMetadata, metadata?: WorkspaceEditEntryMetadata): void; } - export interface NotebookEditorCellEdit { + export interface NotebookEditorEdit { + + replaceNotebookMetadata(value: NotebookDocumentMetadata): void; replaceCells(start: number, end: number, cells: NotebookCellData[]): void; - replaceOutput(index: number, outputs: CellOutput[]): void; - replaceMetadata(index: number, metadata: NotebookCellMetadata): void; + replaceCellOutput(index: number, outputs: CellOutput[]): void; + replaceCellMetadata(index: number, metadata: NotebookCellMetadata): void; + /** @deprecated */ + replaceOutput(index: number, outputs: CellOutput[]): void; + /** @deprecated */ + replaceMetadata(index: number, metadata: NotebookCellMetadata): void; /** @deprecated */ insert(index: number, content: string | string[], language: string, type: CellKind, outputs: CellOutput[], metadata: NotebookCellMetadata | undefined): void; /** @deprecated */ @@ -1448,7 +1455,7 @@ declare module 'vscode' { */ asWebviewUri(localResource: Uri): Uri; - edit(callback: (editBuilder: NotebookEditorCellEdit) => void): Thenable; + edit(callback: (editBuilder: NotebookEditorEdit) => void): Thenable; revealRange(range: NotebookCellRange, revealType?: NotebookEditorRevealType): void; } @@ -1463,6 +1470,10 @@ declare module 'vscode' { outputId: string; } + export interface NotebookDocumentMetadataChangeEvent { + readonly document: NotebookDocument; + } + export interface NotebookCellsChangeData { readonly start: number; readonly deletedCount: number; @@ -1740,6 +1751,7 @@ declare module 'vscode' { export const onDidChangeActiveNotebookEditor: Event; export const onDidChangeNotebookEditorSelection: Event; export const onDidChangeNotebookEditorVisibleRanges: Event; + export const onDidChangeNotebookDocumentMetadata: Event; export const onDidChangeNotebookCells: Event; export const onDidChangeCellOutputs: Event; export const onDidChangeCellLanguage: Event; diff --git a/src/vs/workbench/api/browser/extensionHost.contribution.ts b/src/vs/workbench/api/browser/extensionHost.contribution.ts index bfabf000891..9e264fb33b9 100644 --- a/src/vs/workbench/api/browser/extensionHost.contribution.ts +++ b/src/vs/workbench/api/browser/extensionHost.contribution.ts @@ -15,6 +15,7 @@ import { TokenClassificationExtensionPoints } from 'vs/workbench/services/themes import { LanguageConfigurationFileHandler } from 'vs/workbench/contrib/codeEditor/browser/languageConfigurationExtensionPoint'; // --- mainThread participants +import './mainThreadBulkEdits'; import './mainThreadCodeInsets'; import './mainThreadClipboard'; import './mainThreadCommands'; diff --git a/src/vs/workbench/api/browser/mainThreadBulkEdits.ts b/src/vs/workbench/api/browser/mainThreadBulkEdits.ts new file mode 100644 index 00000000000..2c10477e0d1 --- /dev/null +++ b/src/vs/workbench/api/browser/mainThreadBulkEdits.ts @@ -0,0 +1,44 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IBulkEditService, ResourceEdit, ResourceFileEdit, ResourceTextEdit } from 'vs/editor/browser/services/bulkEditService'; +import { IExtHostContext, IWorkspaceEditDto, WorkspaceEditType, MainThreadBulkEditsShape, MainContext } from 'vs/workbench/api/common/extHost.protocol'; +import { revive } from 'vs/base/common/marshalling'; +import { ResourceNotebookCellEdit } from 'vs/workbench/contrib/bulkEdit/browser/bulkCellEdits'; +import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; + +function reviveWorkspaceEditDto2(data: IWorkspaceEditDto | undefined): ResourceEdit[] { + if (!data?.edits) { + return []; + } + + const result: ResourceEdit[] = []; + for (let edit of revive(data).edits) { + if (edit._type === WorkspaceEditType.File) { + result.push(new ResourceFileEdit(edit.oldUri, edit.newUri, edit.options, edit.metadata)); + } else if (edit._type === WorkspaceEditType.Text) { + result.push(new ResourceTextEdit(edit.resource, edit.edit, edit.modelVersionId, edit.metadata)); + } else if (edit._type === WorkspaceEditType.Cell) { + result.push(new ResourceNotebookCellEdit(edit.resource, edit.edit, edit.notebookMetadata, edit.notebookVersionId, edit.metadata)); + } + } + return result; +} + +@extHostNamedCustomer(MainContext.MainThreadBulkEdits) +export class MainThreadBulkEdits implements MainThreadBulkEditsShape { + + constructor( + _extHostContext: IExtHostContext, + @IBulkEditService private readonly _bulkEditService: IBulkEditService, + ) { } + + dispose(): void { } + + $tryApplyWorkspaceEdit(dto: IWorkspaceEditDto): Promise { + const edits = reviveWorkspaceEditDto2(dto); + return this._bulkEditService.apply(edits).then(() => true, _err => false); + } +} diff --git a/src/vs/workbench/api/browser/mainThreadCustomEditors.ts b/src/vs/workbench/api/browser/mainThreadCustomEditors.ts index 9351d89b4bd..132d36358cd 100644 --- a/src/vs/workbench/api/browser/mainThreadCustomEditors.ts +++ b/src/vs/workbench/api/browser/mainThreadCustomEditors.ts @@ -153,7 +153,7 @@ export class MainThreadCustomEditors extends Disposable implements extHostProtoc return; } - webviewInput.webview.onDispose(() => { + webviewInput.webview.onDidDispose(() => { // If the model is still dirty, make sure we have time to save it if (modelRef.object.isDirty()) { const sub = modelRef.object.onDidChangeDirty(() => { diff --git a/src/vs/workbench/api/browser/mainThreadEditors.ts b/src/vs/workbench/api/browser/mainThreadEditors.ts index d8e199ac0ad..71d1e940085 100644 --- a/src/vs/workbench/api/browser/mainThreadEditors.ts +++ b/src/vs/workbench/api/browser/mainThreadEditors.ts @@ -44,7 +44,7 @@ function reviveWorkspaceEditDto2(data: IWorkspaceEditDto | undefined): ResourceE } else if (edit._type === WorkspaceEditType.Text) { result.push(new ResourceTextEdit(edit.resource, edit.edit, edit.modelVersionId, edit.metadata)); } else if (edit._type === WorkspaceEditType.Cell) { - result.push(new ResourceNotebookCellEdit(edit.resource, edit.edit, edit.modelVersionId, edit.metadata)); + result.push(new ResourceNotebookCellEdit(edit.resource, edit.edit, edit.notebookMetadata, edit.notebookVersionId, edit.metadata)); } } return result; diff --git a/src/vs/workbench/api/browser/mainThreadMessageService.ts b/src/vs/workbench/api/browser/mainThreadMessageService.ts index 74c4109631f..c956da47b95 100644 --- a/src/vs/workbench/api/browser/mainThreadMessageService.ts +++ b/src/vs/workbench/api/browser/mainThreadMessageService.ts @@ -39,9 +39,9 @@ export class MainThreadMessageService implements MainThreadMessageServiceShape { } } - private _showMessage(severity: Severity, message: string, commands: { title: string; isCloseAffordance: boolean; handle: number; }[], extension: IExtensionDescription | undefined): Promise { + private _showMessage(severity: Severity, message: string, commands: { title: string; isCloseAffordance: boolean; handle: number; }[], extension: IExtensionDescription | undefined): Promise { - return new Promise(resolve => { + return new Promise(resolve => { const primaryActions: MessageItemAction[] = []; diff --git a/src/vs/workbench/api/browser/mainThreadNotebook.ts b/src/vs/workbench/api/browser/mainThreadNotebook.ts index ef92249a326..582fd07d165 100644 --- a/src/vs/workbench/api/browser/mainThreadNotebook.ts +++ b/src/vs/workbench/api/browser/mainThreadNotebook.ts @@ -8,6 +8,7 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { Emitter } from 'vs/base/common/event'; import { combinedDisposable, Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; import { ResourceMap } from 'vs/base/common/map'; +import { Schemas } from 'vs/base/common/network'; import { URI, UriComponents } from 'vs/base/common/uri'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -17,9 +18,10 @@ import { INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookB import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; import { INotebookCellStatusBarService } from 'vs/workbench/contrib/notebook/common/notebookCellStatusBarService'; -import { ACCESSIBLE_NOTEBOOK_DISPLAY_ORDER, CellEditType, CellKind, DisplayOrderKey, ICellEditOperation, ICellRange, IEditor, IMainCellDto, INotebookDocumentFilter, NotebookCellMetadata, NotebookCellOutputsSplice, NotebookCellsChangeType, NotebookDocumentMetadata, NOTEBOOK_DISPLAY_ORDER, TransientMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { ACCESSIBLE_NOTEBOOK_DISPLAY_ORDER, CellEditType, DisplayOrderKey, ICellEditOperation, ICellRange, IEditor, IMainCellDto, INotebookDocumentFilter, NotebookCellOutputsSplice, NotebookCellsChangeType, NotebookDocumentMetadata, NOTEBOOK_DISPLAY_ORDER, TransientMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { IMainNotebookController, INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { ExtHostContext, ExtHostNotebookShape, IExtHostContext, INotebookCellStatusBarEntryDto, INotebookDocumentsAndEditorsDelta, INotebookModelAddedData, MainContext, MainThreadNotebookShape, NotebookEditorRevealType, NotebookExtensionDescription } from '../common/extHost.protocol'; class DocumentAndEditorState { @@ -146,29 +148,29 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo @IEditorService private readonly editorService: IEditorService, @IAccessibilityService private readonly accessibilityService: IAccessibilityService, @ILogService private readonly logService: ILogService, - @INotebookCellStatusBarService private readonly cellStatusBarService: INotebookCellStatusBarService + @INotebookCellStatusBarService private readonly cellStatusBarService: INotebookCellStatusBarService, + @IWorkingCopyService private readonly _workingCopyService: IWorkingCopyService, ) { super(); this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostNotebook); this.registerListeners(); } - async $tryApplyEdits(viewType: string, resource: UriComponents, modelVersionId: number, edits: ICellEditOperation[]): Promise { + async $tryApplyEdits(_viewType: string, resource: UriComponents, modelVersionId: number, cellEdits: ICellEditOperation[], newMetadata: NotebookDocumentMetadata | undefined): Promise { const textModel = this._notebookService.getNotebookTextModel(URI.from(resource)); - if (textModel) { - this._notebookService.transformEditsOutputs(textModel, edits); - return textModel.applyEdit(modelVersionId, edits, true); + if (!textModel) { + return false; } - - return false; - } - - async removeNotebookTextModel(uri: URI): Promise { - // TODO@rebornix remove this? obsolete? - // TODO@rebornix, remove cell should use emitDelta as well to ensure document/editor events are sent together - this._proxy.$acceptDocumentAndEditorsDelta({ removedDocuments: [uri] }); - this._documentEventListenersMapping.get(uri)?.dispose(); - this._documentEventListenersMapping.delete(uri); + this._notebookService.transformEditsOutputs(textModel, cellEdits); + if (newMetadata) { + textModel.applyEdit(textModel.versionId, [ + { + editType: CellEditType.DocumentMetadata, + metadata: newMetadata + } + ], true); + } + return textModel.applyEdit(modelVersionId, cellEdits, true); } private _isDeltaEmpty(delta: INotebookDocumentsAndEditorsDelta) { @@ -299,10 +301,19 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo } : e ); - this._proxy.$acceptModelChanged(textModel.uri, data, textModel.isDirty); - this._proxy.$acceptDocumentPropertiesChanged(textModel.uri, { metadata: null }); - })); + /** + * TODO@rebornix, @jrieken + * When a document is modified, it will trigger onDidChangeContent events. + * The first event listener is this one, which doesn't know if the text model is dirty or not. It can ask `workingCopyService` but get the wrong result + * The second event listener is `NotebookEditorModel`, which will then set `isDirty` to `true`. + * Since `e.transient` decides if the model should be dirty or not, we will use the same logic here. + */ + this._proxy.$acceptModelChanged(textModel.uri, data, !e.transient); + if (e.kind === NotebookCellsChangeType.ChangeDocumentMetadata) { + this._proxy.$acceptDocumentPropertiesChanged(textModel.uri, { metadata: textModel.metadata }); + } + })); this._editorEventListenersMapping.set(textModel!.uri.toString(), disposableStore); } }; @@ -452,22 +463,12 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo }); }); }, - createNotebook: async (textModel: NotebookTextModel, backupId?: string) => { - // open notebook document - const data = await this._proxy.$resolveNotebookData(textModel.viewType, textModel.uri, backupId); - textModel.updateLanguages(data.languages); - textModel.metadata = data.metadata; - textModel.transientOptions = options; - - if (data.cells.length) { - textModel.initialize(data!.cells); - } else { - const mainCell = textModel.createCellTextModel('', textModel.resolvedLanguages.length ? textModel.resolvedLanguages[0] : '', CellKind.Code, [], undefined); - textModel.insertTemplateCell(mainCell); - } - - this._proxy.$acceptDocumentPropertiesChanged(textModel.uri, { metadata: textModel.metadata }); - return; + resolveNotebookDocument: async (viewType: string, uri: URI, backupId?: string) => { + const data = await this._proxy.$resolveNotebookData(viewType, uri, backupId); + return { + data, + transientOptions: options + }; }, resolveNotebookEditor: async (viewType: string, uri: URI, editorId: string) => { await this._proxy.$resolveNotebookEditor(viewType, uri, editorId); @@ -475,9 +476,6 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo onDidReceiveMessage: (editorId: string, rendererType: string | undefined, message: unknown) => { this._proxy.$onDidReceiveMessage(editorId, rendererType, message); }, - removeNotebookDocument: async (uri: URI) => { - return this.removeNotebookTextModel(uri); - }, save: async (uri: URI, token: CancellationToken) => { return this._proxy.$saveNotebook(viewType, uri, token); }, @@ -567,27 +565,6 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo textModel?.updateLanguages(languages); } - async $updateNotebookMetadata(viewType: string, resource: UriComponents, metadata: NotebookDocumentMetadata): Promise { - this.logService.debug('MainThreadNotebooks#updateNotebookMetadata', resource.path, metadata); - const textModel = this._notebookService.getNotebookTextModel(URI.from(resource)); - textModel?.updateNotebookMetadata(metadata); - } - - async $updateNotebookCellMetadata(viewType: string, resource: UriComponents, handle: number, metadata: NotebookCellMetadata): Promise { - this.logService.debug('MainThreadNotebooks#updateNotebookCellMetadata', resource.path, handle, metadata); - const textModel = this._notebookService.getNotebookTextModel(URI.from(resource)); - if (!textModel) { - return; - } - - const index = textModel.cells.findIndex(cell => cell.handle === handle); - if (index < 0) { - return; - } - - textModel.applyEdit(textModel.versionId, [{ editType: CellEditType.Metadata, index, metadata }], true); - } - async $spliceNotebookCellOutputs(viewType: string, resource: UriComponents, cellHandle: number, splices: NotebookCellOutputsSplice[]): Promise { this.logService.debug('MainThreadNotebooks#spliceNotebookCellOutputs', resource.path, cellHandle); const textModel = this._notebookService.getNotebookTextModel(URI.from(resource)); @@ -612,10 +589,12 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo const textModel = this._notebookService.getNotebookTextModel(URI.from(resource)); if (textModel) { - textModel.handleEdit(label, () => { - return this._proxy.$undoNotebook(textModel.viewType, textModel.uri, editId, textModel.isDirty); + textModel.handleUnknownEdit(label, () => { + const isDirty = this._workingCopyService.isDirty(textModel.uri.with({ scheme: Schemas.vscodeNotebook })); + return this._proxy.$undoNotebook(textModel.viewType, textModel.uri, editId, isDirty); }, () => { - return this._proxy.$redoNotebook(textModel.viewType, textModel.uri, editId, textModel.isDirty); + const isDirty = this._workingCopyService.isDirty(textModel.uri.with({ scheme: Schemas.vscodeNotebook })); + return this._proxy.$redoNotebook(textModel.viewType, textModel.uri, editId, isDirty); }); } } diff --git a/src/vs/workbench/api/browser/mainThreadProgress.ts b/src/vs/workbench/api/browser/mainThreadProgress.ts index e3b6422e891..f038168e205 100644 --- a/src/vs/workbench/api/browser/mainThreadProgress.ts +++ b/src/vs/workbench/api/browser/mainThreadProgress.ts @@ -73,7 +73,7 @@ export class MainThreadProgress implements MainThreadProgressShape { private _createTask(handle: number) { return (progress: IProgress) => { - return new Promise(resolve => { + return new Promise(resolve => { this._progress.set(handle, { resolve, progress }); }); }; diff --git a/src/vs/workbench/api/browser/mainThreadWebviewPanels.ts b/src/vs/workbench/api/browser/mainThreadWebviewPanels.ts index 84107c980a3..f8541ed9a80 100644 --- a/src/vs/workbench/api/browser/mainThreadWebviewPanels.ts +++ b/src/vs/workbench/api/browser/mainThreadWebviewPanels.ts @@ -140,7 +140,7 @@ export class MainThreadWebviewPanels extends Disposable implements extHostProtoc this._webviewInputs.add(handle, input); this._mainThreadWebviews.addWebview(handle, input.webview); - input.webview.onDispose(() => { + input.webview.onDidDispose(() => { this._proxy.$onDidDisposeWebviewPanel(handle).finally(() => { this._webviewInputs.delete(handle); }); diff --git a/src/vs/workbench/api/browser/mainThreadWebviewViews.ts b/src/vs/workbench/api/browser/mainThreadWebviewViews.ts index eae46c714d0..d43016bcd07 100644 --- a/src/vs/workbench/api/browser/mainThreadWebviewViews.ts +++ b/src/vs/workbench/api/browser/mainThreadWebviewViews.ts @@ -78,7 +78,7 @@ export class MainThreadWebviewsViews extends Disposable implements extHostProtoc }); try { - await this._proxy.$resolveWebviewView(handle, viewType, state, cancellation); + await this._proxy.$resolveWebviewView(handle, viewType, webviewView.title, state, cancellation); } catch (error) { onUnexpectedError(error); webviewView.webview.html = this.mainThreadWebviews.getWebviewResolvedFailedContent(viewType); diff --git a/src/vs/workbench/api/browser/mainThreadWebviews.ts b/src/vs/workbench/api/browser/mainThreadWebviews.ts index 8180b9972ee..904279d527a 100644 --- a/src/vs/workbench/api/browser/mainThreadWebviews.ts +++ b/src/vs/workbench/api/browser/mainThreadWebviews.ts @@ -69,7 +69,7 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma disposables.add(webview.onMessage((message: any) => { this._proxy.$onMessage(handle, message); })); disposables.add(webview.onMissingCsp((extension: ExtensionIdentifier) => this._proxy.$onMissingCsp(handle, extension.value))); - disposables.add(webview.onDispose(() => { + disposables.add(webview.onDidDispose(() => { disposables.dispose(); this._webviews.delete(handle); })); diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 3595cd3e381..46a0b734f5b 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -79,6 +79,7 @@ import { IExtHostConsumerFileSystem } from 'vs/workbench/api/common/extHostFileS import { ExtHostWebviewViews } from 'vs/workbench/api/common/extHostWebviewView'; import { ExtHostCustomEditors } from 'vs/workbench/api/common/extHostCustomEditors'; import { ExtHostWebviewPanels } from 'vs/workbench/api/common/extHostWebviewPanels'; +import { ExtHostBulkEdits } from 'vs/workbench/api/common/extHostBulkEdits'; export interface IExtensionApiFactory { (extension: IExtensionDescription, registry: ExtensionDescriptionRegistry, configProvider: ExtHostConfigProvider): typeof vscode; @@ -127,9 +128,9 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I const extHostUrls = rpcProtocol.set(ExtHostContext.ExtHostUrls, new ExtHostUrls(rpcProtocol)); const extHostDocuments = rpcProtocol.set(ExtHostContext.ExtHostDocuments, new ExtHostDocuments(rpcProtocol, extHostDocumentsAndEditors)); const extHostDocumentContentProviders = rpcProtocol.set(ExtHostContext.ExtHostDocumentContentProviders, new ExtHostDocumentContentProvider(rpcProtocol, extHostDocumentsAndEditors, extHostLogService)); - const extHostDocumentSaveParticipant = rpcProtocol.set(ExtHostContext.ExtHostDocumentSaveParticipant, new ExtHostDocumentSaveParticipant(extHostLogService, extHostDocuments, rpcProtocol.getProxy(MainContext.MainThreadTextEditors))); + const extHostDocumentSaveParticipant = rpcProtocol.set(ExtHostContext.ExtHostDocumentSaveParticipant, new ExtHostDocumentSaveParticipant(extHostLogService, extHostDocuments, rpcProtocol.getProxy(MainContext.MainThreadBulkEdits))); const extHostNotebook = rpcProtocol.set(ExtHostContext.ExtHostNotebook, new ExtHostNotebookController(rpcProtocol, extHostCommands, extHostDocumentsAndEditors, initData.environment, extHostLogService, extensionStoragePaths)); - const extHostEditors = rpcProtocol.set(ExtHostContext.ExtHostEditors, new ExtHostEditors(rpcProtocol, extHostDocumentsAndEditors, extHostNotebook)); + const extHostEditors = rpcProtocol.set(ExtHostContext.ExtHostEditors, new ExtHostEditors(rpcProtocol, extHostDocumentsAndEditors)); const extHostTreeViews = rpcProtocol.set(ExtHostContext.ExtHostTreeViews, new ExtHostTreeViews(rpcProtocol.getProxy(MainContext.MainThreadTreeViews), extHostCommands, extHostLogService)); const extHostEditorInsets = rpcProtocol.set(ExtHostContext.ExtHostEditorInsets, new ExtHostEditorInsets(rpcProtocol.getProxy(MainContext.MainThreadEditorInsets), extHostEditors, initData.environment)); const extHostDiagnostics = rpcProtocol.set(ExtHostContext.ExtHostDiagnostics, new ExtHostDiagnostics(rpcProtocol, extHostLogService)); @@ -154,6 +155,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I rpcProtocol.assertRegistered(expected); // Other instances + const extHostBulkEdits = new ExtHostBulkEdits(rpcProtocol, extHostDocumentsAndEditors, extHostNotebook); const extHostClipboard = new ExtHostClipboard(rpcProtocol); const extHostMessageService = new ExtHostMessageService(rpcProtocol, extHostLogService); const extHostDialogs = new ExtHostDialogs(rpcProtocol); @@ -691,7 +693,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I return extHostWorkspace.saveAll(includeUntitled); }, applyEdit(edit: vscode.WorkspaceEdit): Thenable { - return extHostEditors.applyWorkspaceEdit(edit); + return extHostBulkEdits.applyWorkspaceEdit(edit); }, createFileSystemWatcher: (pattern, ignoreCreate, ignoreChange, ignoreDelete): vscode.FileSystemWatcher => { return extHostFileSystemEvent.createFileSystemWatcher(typeConverters.GlobPattern.from(pattern), ignoreCreate, ignoreChange, ignoreDelete); @@ -969,6 +971,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I checkProposedApiEnabled(extension); return extHostNotebook.onDidChangeActiveNotebookEditor(listener, thisArgs, disposables); }, + onDidChangeNotebookDocumentMetadata(listener, thisArgs?, disposables?) { + checkProposedApiEnabled(extension); + return extHostNotebook.onDidChangeNotebookDocumentMetadata(listener, thisArgs, disposables); + }, onDidChangeNotebookCells(listener, thisArgs?, disposables?) { checkProposedApiEnabled(extension); return extHostNotebook.onDidChangeNotebookCells(listener, thisArgs, disposables); diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index da717e7f0e1..dd896025d43 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -263,6 +263,10 @@ export interface ITextDocumentShowOptions { selection?: IRange; } +export interface MainThreadBulkEditsShape extends IDisposable { + $tryApplyWorkspaceEdit(workspaceEditDto: IWorkspaceEditDto): Promise; +} + export interface MainThreadTextEditorsShape extends IDisposable { $tryShowTextDocument(resource: UriComponents, options: ITextDocumentShowOptions): Promise; $registerTextEditorDecorationType(key: string, options: editorCommon.IDecorationRenderOptions): void; @@ -275,7 +279,6 @@ export interface MainThreadTextEditorsShape extends IDisposable { $tryRevealRange(id: string, range: IRange, revealType: TextEditorRevealType): Promise; $trySetSelections(id: string, selections: ISelection[]): Promise; $tryApplyEdits(id: string, modelVersionId: number, edits: ISingleEditOperation[], opts: IApplyEditsOptions): Promise; - $tryApplyWorkspaceEdit(workspaceEditDto: IWorkspaceEditDto): Promise; $tryInsertSnippet(id: string, template: string, selections: readonly IRange[], opts: IUndoStopOptions): Promise; $getDiffInformation(id: string): Promise; } @@ -683,7 +686,7 @@ export interface ExtHostCustomEditorsShape { } export interface ExtHostWebviewViewsShape { - $resolveWebviewView(webviewHandle: WebviewHandle, viewType: string, state: any, cancellation: CancellationToken): Promise; + $resolveWebviewView(webviewHandle: WebviewHandle, viewType: string, title: string | undefined, state: any, cancellation: CancellationToken): Promise; $onDidChangeWebviewViewVisibility(webviewHandle: WebviewHandle, visible: boolean): void; @@ -738,10 +741,8 @@ export interface MainThreadNotebookShape extends IDisposable { $registerNotebookKernelProvider(extension: NotebookExtensionDescription, handle: number, documentFilter: INotebookDocumentFilter): Promise; $unregisterNotebookKernelProvider(handle: number): Promise; $onNotebookKernelChange(handle: number, uri: UriComponents | undefined): void; - $tryApplyEdits(viewType: string, resource: UriComponents, modelVersionId: number, edits: ICellEditOperation[]): Promise; + $tryApplyEdits(viewType: string, resource: UriComponents, modelVersionId: number, edits: ICellEditOperation[], metadata: NotebookDocumentMetadata | undefined): Promise; $updateNotebookLanguages(viewType: string, resource: UriComponents, languages: string[]): Promise; - $updateNotebookMetadata(viewType: string, resource: UriComponents, metadata: NotebookDocumentMetadata): Promise; - $updateNotebookCellMetadata(viewType: string, resource: UriComponents, handle: number, metadata: NotebookCellMetadata | undefined): Promise; $spliceNotebookCellOutputs(viewType: string, resource: UriComponents, cellHandle: number, splices: NotebookCellOutputsSplice[]): Promise; $postMessage(editorId: string, forRendererId: string | undefined, value: any): Promise; $setStatusBarEntry(id: number, statusBarEntry: INotebookCellStatusBarEntryDto): Promise; @@ -1274,8 +1275,9 @@ export interface IWorkspaceTextEditDto { export interface IWorkspaceCellEditDto { _type: WorkspaceEditType.Cell; resource: UriComponents; - edit: ICellEditOperation; - modelVersionId?: number; + edit?: ICellEditOperation; + notebookMetadata?: NotebookDocumentMetadata; + notebookVersionId?: number; metadata?: IWorkspaceEditEntryMetadataDto; } @@ -1725,6 +1727,7 @@ export interface ExtHostTimelineShape { export const MainContext = { MainThreadAuthentication: createMainId('MainThreadAuthentication'), + MainThreadBulkEdits: createMainId('MainThreadBulkEdits'), MainThreadClipboard: createMainId('MainThreadClipboard'), MainThreadCommands: createMainId('MainThreadCommands'), MainThreadComments: createMainId('MainThreadComments'), diff --git a/src/vs/workbench/api/common/extHostApiCommands.ts b/src/vs/workbench/api/common/extHostApiCommands.ts index 1f60c7f5ad0..e7cac07ee3b 100644 --- a/src/vs/workbench/api/common/extHostApiCommands.ts +++ b/src/vs/workbench/api/common/extHostApiCommands.ts @@ -228,10 +228,15 @@ const newCommands: ApiCommand[] = [ } return typeConverters.WorkspaceEdit.to(value); }) + ), + // --- links + new ApiCommand( + 'vscode.executeLinkProvider', '_executeLinkProvider', 'Execute document link provider.', + [ApiCommandArgument.Uri, new ApiCommandArgument('linkResolveCount', '(optional) Number of links that should be resolved, only when links are unresolved.', v => typeof v === 'number' || typeof v === 'undefined', v => v)], + new ApiCommandResult('A promise that resolves to an array of DocumentLink-instances.', value => value.map(typeConverters.DocumentLink.to)) ) ]; - //#endregion @@ -289,13 +294,6 @@ export class ExtHostApiCommands { returns: 'A promise that resolves to an array of CodeLens-instances.' }); - this._register('vscode.executeLinkProvider', this._executeDocumentLinkProvider, { - description: 'Execute document link provider.', - args: [ - { name: 'uri', description: 'Uri of a text document', constraint: URI } - ], - returns: 'A promise that resolves to an array of DocumentLink-instances.' - }); this._register('vscode.executeDocumentColorProvider', this._executeDocumentColorProvider, { description: 'Execute document color provider.', args: [ @@ -478,12 +476,6 @@ export class ExtHostApiCommands { typeConverters.Range.to(item.range), item.command ? this._commands.converter.fromInternal(item.command) : undefined); })); - - } - - private _executeDocumentLinkProvider(resource: URI): Promise { - return this._commands.executeCommand('_executeLinkProvider', resource) - .then(tryMapWith(typeConverters.DocumentLink.to)); } } diff --git a/src/vs/workbench/api/common/extHostBulkEdits.ts b/src/vs/workbench/api/common/extHostBulkEdits.ts new file mode 100644 index 00000000000..c521b1e6974 --- /dev/null +++ b/src/vs/workbench/api/common/extHostBulkEdits.ts @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { MainContext, MainThreadBulkEditsShape } from 'vs/workbench/api/common/extHost.protocol'; +import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; +import { ExtHostNotebookController } from 'vs/workbench/api/common/extHostNotebook'; +import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; +import { WorkspaceEdit } from 'vs/workbench/api/common/extHostTypeConverters'; +import type * as vscode from 'vscode'; + +export class ExtHostBulkEdits { + + private readonly _proxy: MainThreadBulkEditsShape; + + constructor( + @IExtHostRpcService extHostRpc: IExtHostRpcService, + private readonly _extHostDocumentsAndEditors: ExtHostDocumentsAndEditors, + private readonly _extHostNotebooks: ExtHostNotebookController, + ) { + this._proxy = extHostRpc.getProxy(MainContext.MainThreadBulkEdits); + } + + applyWorkspaceEdit(edit: vscode.WorkspaceEdit): Promise { + const dto = WorkspaceEdit.from(edit, this._extHostDocumentsAndEditors, this._extHostNotebooks); + return this._proxy.$tryApplyWorkspaceEdit(dto); + } +} diff --git a/src/vs/workbench/api/common/extHostDocumentSaveParticipant.ts b/src/vs/workbench/api/common/extHostDocumentSaveParticipant.ts index 5613e66db18..fe6fd330378 100644 --- a/src/vs/workbench/api/common/extHostDocumentSaveParticipant.ts +++ b/src/vs/workbench/api/common/extHostDocumentSaveParticipant.ts @@ -6,7 +6,7 @@ import { Event } from 'vs/base/common/event'; import { URI, UriComponents } from 'vs/base/common/uri'; import { illegalState } from 'vs/base/common/errors'; -import { ExtHostDocumentSaveParticipantShape, MainThreadTextEditorsShape, IWorkspaceEditDto, WorkspaceEditType } from 'vs/workbench/api/common/extHost.protocol'; +import { ExtHostDocumentSaveParticipantShape, IWorkspaceEditDto, WorkspaceEditType, MainThreadBulkEditsShape } from 'vs/workbench/api/common/extHost.protocol'; import { TextEdit } from 'vs/workbench/api/common/extHostTypes'; import { Range, TextDocumentSaveReason, EndOfLine } from 'vs/workbench/api/common/extHostTypeConverters'; import { ExtHostDocuments } from 'vs/workbench/api/common/extHostDocuments'; @@ -26,7 +26,7 @@ export class ExtHostDocumentSaveParticipant implements ExtHostDocumentSavePartic constructor( private readonly _logService: ILogService, private readonly _documents: ExtHostDocuments, - private readonly _mainThreadEditors: MainThreadTextEditorsShape, + private readonly _mainThreadBulkEdits: MainThreadBulkEditsShape, private readonly _thresholds: { timeout: number; errors: number; } = { timeout: 1500, errors: 3 } ) { // @@ -165,7 +165,7 @@ export class ExtHostDocumentSaveParticipant implements ExtHostDocumentSavePartic } if (version === document.version) { - return this._mainThreadEditors.$tryApplyWorkspaceEdit(dto); + return this._mainThreadBulkEdits.$tryApplyWorkspaceEdit(dto); } return Promise.reject(new Error('concurrent_edits')); diff --git a/src/vs/workbench/api/common/extHostExtensionService.ts b/src/vs/workbench/api/common/extHostExtensionService.ts index 0bb5188614b..33b062b329c 100644 --- a/src/vs/workbench/api/common/extHostExtensionService.ts +++ b/src/vs/workbench/api/common/extHostExtensionService.ts @@ -747,7 +747,7 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme protected abstract _beforeAlmostReadyToRunExtensions(): Promise; protected abstract _getEntryPoint(extensionDescription: IExtensionDescription): string | undefined; protected abstract _loadCommonJSModule(module: URI, activationTimesBuilder: ExtensionActivationTimesBuilder): Promise; - public abstract async $setRemoteEnvironment(env: { [key: string]: string | null }): Promise; + public abstract $setRemoteEnvironment(env: { [key: string]: string | null }): Promise; } diff --git a/src/vs/workbench/api/common/extHostFileSystemEventService.ts b/src/vs/workbench/api/common/extHostFileSystemEventService.ts index 76a5f2f81e9..c576f8f5e4c 100644 --- a/src/vs/workbench/api/common/extHostFileSystemEventService.ts +++ b/src/vs/workbench/api/common/extHostFileSystemEventService.ts @@ -8,7 +8,7 @@ import { IRelativePattern, parse } from 'vs/base/common/glob'; import { URI } from 'vs/base/common/uri'; import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; import type * as vscode from 'vscode'; -import { ExtHostFileSystemEventServiceShape, FileSystemEvents, IMainContext, MainContext, MainThreadTextEditorsShape, SourceTargetPair, IWorkspaceEditDto } from './extHost.protocol'; +import { ExtHostFileSystemEventServiceShape, FileSystemEvents, IMainContext, MainContext, SourceTargetPair, IWorkspaceEditDto, MainThreadBulkEditsShape } from './extHost.protocol'; import * as typeConverter from './extHostTypeConverters'; import { Disposable, WorkspaceEdit } from './extHostTypes'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; @@ -123,7 +123,7 @@ export class ExtHostFileSystemEventService implements ExtHostFileSystemEventServ mainContext: IMainContext, private readonly _logService: ILogService, private readonly _extHostDocumentsAndEditors: ExtHostDocumentsAndEditors, - private readonly _mainThreadTextEditors: MainThreadTextEditorsShape = mainContext.getProxy(MainContext.MainThreadTextEditors) + private readonly _mainThreadBulkEdits: MainThreadBulkEditsShape = mainContext.getProxy(MainContext.MainThreadBulkEdits) ) { // } @@ -222,7 +222,7 @@ export class ExtHostFileSystemEventService implements ExtHostFileSystemEventServ let { edits } = typeConverter.WorkspaceEdit.from(edit, this._extHostDocumentsAndEditors); dto.edits = dto.edits.concat(edits); } - return this._mainThreadTextEditors.$tryApplyWorkspaceEdit(dto); + return this._mainThreadBulkEdits.$tryApplyWorkspaceEdit(dto); } } } diff --git a/src/vs/workbench/api/common/extHostNotebook.ts b/src/vs/workbench/api/common/extHostNotebook.ts index 03e7cedad80..128cdec9c14 100644 --- a/src/vs/workbench/api/common/extHostNotebook.ts +++ b/src/vs/workbench/api/common/extHostNotebook.ts @@ -4,562 +4,24 @@ *--------------------------------------------------------------------------------------------*/ import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; -import { readonly } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; -import { hash } from 'vs/base/common/hash'; -import { Disposable, DisposableStore, dispose, IDisposable } from 'vs/base/common/lifecycle'; -import { Schemas } from 'vs/base/common/network'; -import { joinPath } from 'vs/base/common/resources'; -import { ISplice } from 'vs/base/common/sequence'; +import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { URI, UriComponents } from 'vs/base/common/uri'; import * as UUID from 'vs/base/common/uuid'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; -import { CellKind, ExtHostNotebookShape, ICommandDto, IMainContext, IModelAddedData, INotebookDocumentPropertiesChangeData, INotebookDocumentsAndEditorsDelta, INotebookEditorPropertiesChangeData, MainContext, MainThreadNotebookShape, NotebookCellOutputsSplice } from 'vs/workbench/api/common/extHost.protocol'; +import { ExtHostNotebookShape, ICommandDto, IMainContext, IModelAddedData, INotebookDocumentPropertiesChangeData, INotebookDocumentsAndEditorsDelta, INotebookEditorPropertiesChangeData, MainContext, MainThreadBulkEditsShape, MainThreadNotebookShape } from 'vs/workbench/api/common/extHost.protocol'; import { ILogService } from 'vs/platform/log/common/log'; import { CommandsConverter, ExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; -import { ExtHostDocumentsAndEditors, IExtHostModelAddedData } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; +import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; import { IExtensionStoragePaths } from 'vs/workbench/api/common/extHostStoragePaths'; import * as typeConverters from 'vs/workbench/api/common/extHostTypeConverters'; import * as extHostTypes from 'vs/workbench/api/common/extHostTypes'; import { asWebviewUri, WebviewInitData } from 'vs/workbench/api/common/shared/webview'; -import { addIdToOutput, CellEditType, CellOutputKind, CellStatusbarAlignment, CellUri, diff, ICellEditOperation, ICellReplaceEdit, IMainCellDto, INotebookCellStatusBarEntry, INotebookDisplayOrder, INotebookEditData, INotebookKernelInfoDto2, IProcessedOutput, NotebookCellMetadata, NotebookCellsChangedEventDto, NotebookCellsChangeType, NotebookCellsSplice2, NotebookDataDto, notebookDocumentMetadataDefaults } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { addIdToOutput, CellStatusbarAlignment, CellUri, INotebookCellStatusBarEntry, INotebookDisplayOrder, INotebookKernelInfoDto2, NotebookCellMetadata, NotebookCellsChangedEventDto, NotebookCellsChangeType, NotebookDataDto, notebookDocumentMetadataDefaults } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import * as vscode from 'vscode'; -import { Cache } from './cache'; import { ResourceMap } from 'vs/base/common/map'; - -interface IObservable { - proxy: T; - onDidChange: Event; -} - -function getObservable(obj: T): IObservable { - const onDidChange = new Emitter(); - const proxy = new Proxy(obj, { - set(target: T, p: PropertyKey, value: any, _receiver: any): boolean { - target[p as keyof T] = value; - onDidChange.fire(); - return true; - } - }); - - return { - proxy, - onDidChange: onDidChange.event - }; -} - -interface INotebookEventEmitter { - emitModelChange(events: vscode.NotebookCellsChangeEvent): void; - emitCellOutputsChange(event: vscode.NotebookCellOutputsChangeEvent): void; - emitCellLanguageChange(event: vscode.NotebookCellLanguageChangeEvent): void; - emitCellMetadataChange(event: vscode.NotebookCellMetadataChangeEvent): void; -} - -export class ExtHostCell extends Disposable { - - public static asModelAddData(notebook: vscode.NotebookDocument, cell: IMainCellDto): IExtHostModelAddedData { - return { - EOL: cell.eol, - lines: cell.source, - modeId: cell.language, - uri: cell.uri, - isDirty: false, - versionId: 1, - notebook - }; - } - - private _onDidDispose = new Emitter(); - readonly onDidDispose: Event = this._onDidDispose.event; - - private _onDidChangeOutputs = new Emitter[]>(); - readonly onDidChangeOutputs: Event[]> = this._onDidChangeOutputs.event; - - private _outputs: any[]; - private _outputMapping = new WeakMap(); - - private _metadata: vscode.NotebookCellMetadata; - private _metadataChangeListener: IDisposable; - - readonly handle: number; - readonly uri: URI; - readonly cellKind: CellKind; - - private _cell: vscode.NotebookCell | undefined; - - constructor( - private readonly _proxy: MainThreadNotebookShape, - private readonly _notebook: ExtHostNotebookDocument, - private readonly _extHostDocument: ExtHostDocumentsAndEditors, - private readonly _cellData: IMainCellDto, - ) { - super(); - - this.handle = _cellData.handle; - this.uri = URI.revive(_cellData.uri); - this.cellKind = _cellData.cellKind; - - this._outputs = _cellData.outputs; - for (const output of this._outputs) { - this._outputMapping.set(output, output.outputId); - delete output.outputId; - } - - const observableMetadata = getObservable(_cellData.metadata ?? {}); - this._metadata = observableMetadata.proxy; - this._metadataChangeListener = this._register(observableMetadata.onDidChange(() => { - this._updateMetadata(); - })); - } - - get cell(): vscode.NotebookCell { - if (!this._cell) { - const that = this; - const document = this._extHostDocument.getDocument(this.uri)!.document; - this._cell = Object.freeze({ - notebook: that._notebook.notebookDocument, - uri: that.uri, - cellKind: this._cellData.cellKind, - document, - language: document.languageId, - get outputs() { return that._outputs; }, - set outputs(value) { that._updateOutputs(value); }, - get metadata() { return that._metadata; }, - set metadata(value) { - that.setMetadata(value); - that._updateMetadata(); - }, - }); - } - return this._cell; - } - - dispose() { - super.dispose(); - this._onDidDispose.fire(); - } - - setOutputs(newOutputs: vscode.CellOutput[]): void { - this._outputs = newOutputs; - } - - private _updateOutputs(newOutputs: vscode.CellOutput[]) { - const rawDiffs = diff(this._outputs || [], newOutputs || [], (a) => { - return this._outputMapping.has(a); - }); - - const transformedDiffs: ISplice[] = rawDiffs.map(diff => { - for (let i = diff.start; i < diff.start + diff.deleteCount; i++) { - this._outputMapping.delete(this._outputs[i]); - } - - return { - deleteCount: diff.deleteCount, - start: diff.start, - toInsert: diff.toInsert.map((output): IProcessedOutput => { - if (output.outputKind === CellOutputKind.Rich) { - const uuid = UUID.generateUuid(); - this._outputMapping.set(output, uuid); - return { ...output, outputId: uuid }; - } - - this._outputMapping.set(output, undefined); - return output; - }) - }; - }); - - this._outputs = newOutputs; - this._onDidChangeOutputs.fire(transformedDiffs); - } - - setMetadata(newMetadata: vscode.NotebookCellMetadata): void { - // Don't apply metadata defaults here, 'undefined' means 'inherit from document metadata' - this._metadataChangeListener.dispose(); - const observableMetadata = getObservable(newMetadata); - this._metadata = observableMetadata.proxy; - this._metadataChangeListener = this._register(observableMetadata.onDidChange(() => { - this._updateMetadata(); - })); - } - - private _updateMetadata(): Promise { - return this._proxy.$updateNotebookCellMetadata(this._notebook.notebookDocument.viewType, this._notebook.uri, this.handle, this._metadata); - } -} - -class RawContentChangeEvent { - - constructor(readonly start: number, readonly deletedCount: number, readonly deletedItems: ExtHostCell[], readonly items: ExtHostCell[]) { } - - static asApiEvent(event: RawContentChangeEvent): vscode.NotebookCellsChangeData { - return Object.freeze({ - start: event.start, - deletedCount: event.deletedCount, - deletedItems: event.deletedItems.map(data => data.cell), - items: event.items.map(data => data.cell) - }); - } -} - -export class ExtHostNotebookDocument extends Disposable { - - private static _handlePool: number = 0; - readonly handle = ExtHostNotebookDocument._handlePool++; - - private _cells: ExtHostCell[] = []; - - private _cellDisposableMapping = new Map(); - - private _notebook: vscode.NotebookDocument | undefined; - private _metadata: Required; - private _metadataChangeListener: IDisposable; - private _versionId = 0; - private _isDirty: boolean = false; - private _backupCounter = 1; - private _backup?: vscode.NotebookDocumentBackup; - private _disposed = false; - private _languages: string[] = []; - - private readonly _edits = new Cache('notebook documents'); - - constructor( - private readonly _proxy: MainThreadNotebookShape, - private readonly _documentsAndEditors: ExtHostDocumentsAndEditors, - private readonly _emitter: INotebookEventEmitter, - private readonly _viewType: string, - metadata: Required, - public readonly uri: URI, - public readonly renderingHandler: ExtHostNotebookOutputRenderingHandler, - private readonly _storagePath: URI | undefined - ) { - super(); - - const observableMetadata = getObservable(metadata); - this._metadata = observableMetadata.proxy; - this._metadataChangeListener = this._register(observableMetadata.onDidChange(() => { - this._tryUpdateMetadata(); - })); - } - - dispose() { - this._disposed = true; - super.dispose(); - dispose(this._cellDisposableMapping.values()); - } - - private _updateMetadata(newMetadata: Required) { - this._metadataChangeListener.dispose(); - newMetadata = { - ...notebookDocumentMetadataDefaults, - ...newMetadata - }; - if (this._metadataChangeListener) { - this._metadataChangeListener.dispose(); - } - - const observableMetadata = getObservable(newMetadata); - this._metadata = observableMetadata.proxy; - this._metadataChangeListener = this._register(observableMetadata.onDidChange(() => { - this._tryUpdateMetadata(); - })); - - this._tryUpdateMetadata(); - } - - private _tryUpdateMetadata() { - this._proxy.$updateNotebookMetadata(this._viewType, this.uri, this._metadata); - } - get notebookDocument(): vscode.NotebookDocument { - if (!this._notebook) { - const that = this; - this._notebook = Object.freeze({ - get uri() { return that.uri; }, - get version() { return that._versionId; }, - get fileName() { return that.uri.fsPath; }, - get viewType() { return that._viewType; }, - get isDirty() { return that._isDirty; }, - get isUntitled() { return that.uri.scheme === Schemas.untitled; }, - get cells(): ReadonlyArray { return that._cells.map(cell => cell.cell); }, - get languages() { return that._languages; }, - set languages(value: string[]) { that._trySetLanguages(value); }, - get metadata() { return that._metadata; }, - set metadata(value: Required) { that._updateMetadata(value); }, - }); - } - return this._notebook; - } - - private _trySetLanguages(newLanguages: string[]) { - this._languages = newLanguages; - this._proxy.$updateNotebookLanguages(this._viewType, this.uri, this._languages); - } - - getNewBackupUri(): URI { - if (!this._storagePath) { - throw new Error('Backup requires a valid storage path'); - } - const fileName = hashPath(this.uri) + (this._backupCounter++); - return joinPath(this._storagePath, fileName); - } - - updateBackup(backup: vscode.NotebookDocumentBackup): void { - this._backup?.delete(); - this._backup = backup; - } - - disposeBackup(): void { - this._backup?.delete(); - this._backup = undefined; - } - - acceptModelChanged(event: NotebookCellsChangedEventDto, isDirty: boolean): void { - this._versionId = event.versionId; - this._isDirty = isDirty; - if (event.kind === NotebookCellsChangeType.Initialize) { - this._spliceNotebookCells(event.changes, true); - } if (event.kind === NotebookCellsChangeType.ModelChange) { - this._spliceNotebookCells(event.changes, false); - } else if (event.kind === NotebookCellsChangeType.Move) { - this._moveCell(event.index, event.newIdx); - } else if (event.kind === NotebookCellsChangeType.Output) { - this._setCellOutputs(event.index, event.outputs); - } else if (event.kind === NotebookCellsChangeType.ChangeLanguage) { - this._changeCellLanguage(event.index, event.language); - } else if (event.kind === NotebookCellsChangeType.ChangeCellMetadata) { - this._changeCellMetadata(event.index, event.metadata); - } - } - - private _spliceNotebookCells(splices: NotebookCellsSplice2[], initialization: boolean): void { - if (this._disposed) { - return; - } - - const contentChangeEvents: RawContentChangeEvent[] = []; - const addedCellDocuments: IExtHostModelAddedData[] = []; - const removedCellDocuments: URI[] = []; - - splices.reverse().forEach(splice => { - const cellDtos = splice[2]; - const newCells = cellDtos.map(cell => { - - const extCell = new ExtHostCell(this._proxy, this, this._documentsAndEditors, cell); - - if (!initialization) { - addedCellDocuments.push(ExtHostCell.asModelAddData(this.notebookDocument, cell)); - } - - if (!this._cellDisposableMapping.has(extCell.handle)) { - const store = new DisposableStore(); - store.add(extCell); - this._cellDisposableMapping.set(extCell.handle, store); - } - - const store = this._cellDisposableMapping.get(extCell.handle)!; - - store.add(extCell.onDidChangeOutputs((diffs) => { - this.eventuallyUpdateCellOutputs(extCell, diffs); - })); - - return extCell; - }); - - for (let j = splice[0]; j < splice[0] + splice[1]; j++) { - this._cellDisposableMapping.get(this._cells[j].handle)?.dispose(); - this._cellDisposableMapping.delete(this._cells[j].handle); - } - - const deletedItems = this._cells.splice(splice[0], splice[1], ...newCells); - for (let cell of deletedItems) { - removedCellDocuments.push(cell.uri); - } - - contentChangeEvents.push(new RawContentChangeEvent(splice[0], splice[1], deletedItems, newCells)); - }); - - this._documentsAndEditors.acceptDocumentsAndEditorsDelta({ - addedDocuments: addedCellDocuments, - removedDocuments: removedCellDocuments - }); - - if (!initialization) { - this._emitter.emitModelChange({ - document: this.notebookDocument, - changes: contentChangeEvents.map(RawContentChangeEvent.asApiEvent) - }); - } - } - - private _moveCell(index: number, newIdx: number): void { - const cells = this._cells.splice(index, 1); - this._cells.splice(newIdx, 0, ...cells); - const changes: vscode.NotebookCellsChangeData[] = [{ - start: index, - deletedCount: 1, - deletedItems: cells.map(data => data.cell), - items: [] - }, { - start: newIdx, - deletedCount: 0, - deletedItems: [], - items: cells.map(data => data.cell) - }]; - this._emitter.emitModelChange({ - document: this.notebookDocument, - changes - }); - } - - private _setCellOutputs(index: number, outputs: IProcessedOutput[]): void { - const cell = this._cells[index]; - cell.setOutputs(outputs); - this._emitter.emitCellOutputsChange({ document: this.notebookDocument, cells: [cell.cell] }); - } - - private _changeCellLanguage(index: number, language: string): void { - const cell = this._cells[index]; - const event: vscode.NotebookCellLanguageChangeEvent = { document: this.notebookDocument, cell: cell.cell, language }; - this._emitter.emitCellLanguageChange(event); - } - - private _changeCellMetadata(index: number, newMetadata: NotebookCellMetadata | undefined): void { - const cell = this._cells[index]; - cell.setMetadata(newMetadata || {}); - const event: vscode.NotebookCellMetadataChangeEvent = { document: this.notebookDocument, cell: cell.cell }; - this._emitter.emitCellMetadataChange(event); - } - - async eventuallyUpdateCellOutputs(cell: ExtHostCell, diffs: ISplice[]) { - const outputDtos: NotebookCellOutputsSplice[] = diffs.map(diff => { - const outputs = diff.toInsert; - return [diff.start, diff.deleteCount, outputs]; - }); - - if (!outputDtos.length) { - return; - } - - await this._proxy.$spliceNotebookCellOutputs(this._viewType, this.uri, cell.handle, outputDtos); - this._emitter.emitCellOutputsChange({ - document: this.notebookDocument, - cells: [cell.cell] - }); - } - - getCell(cellHandle: number): ExtHostCell | undefined { - return this._cells.find(cell => cell.handle === cellHandle); - } - - - addEdit(item: vscode.NotebookDocumentEditEvent): number { - return this._edits.add([item]); - } - - async undo(editId: number, isDirty: boolean): Promise { - await this.getEdit(editId).undo(); - // if (!isDirty) { - // this.disposeBackup(); - // } - } - - async redo(editId: number, isDirty: boolean): Promise { - await this.getEdit(editId).redo(); - // if (!isDirty) { - // this.disposeBackup(); - // } - } - - private getEdit(editId: number): vscode.NotebookDocumentEditEvent { - const edit = this._edits.get(editId, 0); - if (!edit) { - throw new Error('No edit found'); - } - - return edit; - } - - disposeEdits(editIds: number[]): void { - for (const id of editIds) { - this._edits.delete(id); - } - } -} - -export class NotebookEditorCellEditBuilder implements vscode.NotebookEditorCellEdit { - - private readonly _documentVersionId: number; - private readonly _collectedEdits: ICellEditOperation[] = []; - private _finalized: boolean = false; - - constructor(documentVersionId: number) { - this._documentVersionId = documentVersionId; - } - - finalize(): INotebookEditData { - this._finalized = true; - return { - documentVersionId: this._documentVersionId, - edits: this._collectedEdits, - }; - } - - private _throwIfFinalized() { - if (this._finalized) { - throw new Error('Edit is only valid while callback runs'); - } - } - - replaceMetadata(index: number, metadata: vscode.NotebookCellMetadata): void { - this._throwIfFinalized(); - this._collectedEdits.push({ - editType: CellEditType.Metadata, - index, - metadata - }); - } - - replaceOutput(index: number, outputs: vscode.CellOutput[]): void { - this._throwIfFinalized(); - this._collectedEdits.push({ - editType: CellEditType.Output, - index, - outputs: outputs.map(output => addIdToOutput(output)) - }); - } - - replaceCells(from: number, to: number, cells: vscode.NotebookCellData[]): void { - this._throwIfFinalized(); - - this._collectedEdits.push({ - editType: CellEditType.Replace, - index: from, - count: to - from, - cells: cells.map(data => { - return { - ...data, - outputs: data.outputs.map(output => addIdToOutput(output)), - }; - }) - }); - } - - insert(index: number, content: string | string[], language: string, type: CellKind, outputs: vscode.CellOutput[], metadata: vscode.NotebookCellMetadata | undefined): void { - this._throwIfFinalized(); - this.replaceCells(index, index, [{ - language, - outputs, - metadata, - cellKind: type, - source: Array.isArray(content) ? content.join('\n') : content, - }]); - } - - delete(index: number): void { - this._throwIfFinalized(); - this.replaceCells(index, 1, []); - } -} +import { ExtHostCell, ExtHostNotebookDocument } from './extHostNotebookDocument'; +import { ExtHostNotebookEditor } from './extHostNotebookEditor'; class ExtHostWebviewCommWrapper extends Disposable { private readonly _onDidReceiveDocumentMessage = new Emitter(); @@ -606,152 +68,7 @@ class ExtHostWebviewCommWrapper extends Disposable { } } -export class ExtHostNotebookEditor extends Disposable implements vscode.NotebookEditor { - private _viewColumn: vscode.ViewColumn | undefined; - selection?: vscode.NotebookCell; - - private _visibleRanges: vscode.NotebookCellRange[] = []; - - get visibleRanges() { - return this._visibleRanges; - } - - set visibleRanges(_range: vscode.NotebookCellRange[]) { - throw readonly('visibleRanges'); - } - - _acceptVisibleRanges(value: vscode.NotebookCellRange[]): void { - this._visibleRanges = value; - } - - private _active: boolean = false; - get active(): boolean { - return this._active; - } - - set active(_state: boolean) { - throw readonly('active'); - } - - private _visible: boolean = false; - get visible(): boolean { - return this._visible; - } - - set visible(_state: boolean) { - throw readonly('visible'); - } - - _acceptVisibility(value: boolean) { - this._visible = value; - } - - _acceptActive(value: boolean) { - this._active = value; - } - - private _kernel?: vscode.NotebookKernel; - - get kernel() { - return this._kernel; - } - - set kernel(_kernel: vscode.NotebookKernel | undefined) { - throw readonly('kernel'); - } - - private _onDidDispose = new Emitter(); - readonly onDidDispose: Event = this._onDidDispose.event; - private _onDidReceiveMessage = new Emitter(); - onDidReceiveMessage: vscode.Event = this._onDidReceiveMessage.event; - - constructor( - private readonly viewType: string, - readonly id: string, - public uri: URI, - private _proxy: MainThreadNotebookShape, - private _webComm: vscode.NotebookCommunication, - public readonly notebookData: ExtHostNotebookDocument, - ) { - super(); - this._register(this._webComm.onDidReceiveMessage(e => { - this._onDidReceiveMessage.fire(e); - })); - } - - get document(): vscode.NotebookDocument { - return this.notebookData.notebookDocument; - } - - edit(callback: (editBuilder: NotebookEditorCellEditBuilder) => void): Thenable { - const edit = new NotebookEditorCellEditBuilder(this.document.version); - callback(edit); - return this._applyEdit(edit.finalize()); - } - - private _applyEdit(editData: INotebookEditData): Promise { - - // return when there is nothing to do - if (editData.edits.length === 0) { - return Promise.resolve(true); - } - - const compressedEdits: ICellEditOperation[] = []; - let compressedEditsIndex = -1; - - for (let i = 0; i < editData.edits.length; i++) { - if (compressedEditsIndex < 0) { - compressedEdits.push(editData.edits[i]); - compressedEditsIndex++; - continue; - } - - const prevIndex = compressedEditsIndex; - const prev = compressedEdits[prevIndex]; - - if (prev.editType === CellEditType.Replace && editData.edits[i].editType === CellEditType.Replace) { - if (prev.index === editData.edits[i].index) { - prev.cells.push(...(editData.edits[i] as ICellReplaceEdit).cells); - prev.count += (editData.edits[i] as ICellReplaceEdit).count; - continue; - } - } - - compressedEdits.push(editData.edits[i]); - compressedEditsIndex++; - } - - return this._proxy.$tryApplyEdits(this.viewType, this.uri, editData.documentVersionId, compressedEdits); - } - - revealRange(range: vscode.NotebookCellRange, revealType?: extHostTypes.NotebookEditorRevealType) { - this._proxy.$tryRevealRange(this.id, range, revealType || extHostTypes.NotebookEditorRevealType.Default); - } - - get viewColumn(): vscode.ViewColumn | undefined { - return this._viewColumn; - } - - set viewColumn(value) { - throw readonly('viewColumn'); - } - - updateActiveKernel(kernel?: vscode.NotebookKernel) { - this._kernel = kernel; - } - async postMessage(message: any): Promise { - return this._webComm.postMessage(message); - } - - asWebviewUri(localResource: vscode.Uri): vscode.Uri { - return this._webComm.asWebviewUri(localResource); - } - dispose() { - this._onDidDispose.fire(); - super.dispose(); - } -} export interface ExtHostNotebookOutputRenderingHandler { outputDisplayOrder: INotebookDisplayOrder | undefined; @@ -874,6 +191,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN private static _notebookKernelProviderHandlePool: number = 0; private readonly _proxy: MainThreadNotebookShape; + private readonly _mainThreadBulkEdits: MainThreadBulkEditsShape; private readonly _notebookContentProviders = new Map(); private readonly _notebookKernels = new Map(); private readonly _notebookKernelProviders = new Map(); @@ -885,6 +203,8 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN readonly onDidChangeNotebookEditorSelection = this._onDidChangeNotebookEditorSelection.event; private readonly _onDidChangeNotebookEditorVisibleRanges = new Emitter(); readonly onDidChangeNotebookEditorVisibleRanges = this._onDidChangeNotebookEditorVisibleRanges.event; + private readonly _onDidChangeNotebookDocumentMetadata = new Emitter(); + readonly onDidChangeNotebookDocumentMetadata = this._onDidChangeNotebookDocumentMetadata.event; private readonly _onDidChangeNotebookCells = new Emitter(); readonly onDidChangeNotebookCells = this._onDidChangeNotebookCells.event; private readonly _onDidChangeCellOutputs = new Emitter(); @@ -929,6 +249,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN private readonly _extensionStoragePaths: IExtensionStoragePaths, ) { this._proxy = mainContext.getProxy(MainContext.MainThreadNotebook); + this._mainThreadBulkEdits = mainContext.getProxy(MainContext.MainThreadBulkEdits); this._commandsConverter = commands.converter; commands.registerArgumentProcessor({ @@ -1199,7 +520,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN const kernel = event.kernelId ? adapter.getKernel(event.kernelId) : undefined; this._editors.forEach(editor => { if (editor.editor.notebookData === document) { - editor.editor.updateActiveKernel(kernel); + editor.editor._acceptKernel(kernel); } }); this._onDidChangeActiveNotebookKernel.fire({ document: document.notebookDocument, kernel }); @@ -1212,7 +533,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN const uriStr = URI.revive(uriComponents).toString(); let editor: { editor: ExtHostNotebookEditor; } | undefined; this._editors.forEach(e => { - if (e.editor.uri.toString() === uriStr) { + if (e.editor.document.uri.toString() === uriStr) { editor = e; } }); @@ -1285,10 +606,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN } if (data.metadata) { - editor.editor.notebookData.notebookDocument.metadata = { - ...notebookDocumentMetadataDefaults, - ...data.metadata - }; + editor.editor.notebookData.acceptDocumentPropertiesChanged(data); } } @@ -1302,9 +620,8 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN } const editor = new ExtHostNotebookEditor( - document.notebookDocument.viewType, editorId, - revivedUri, + document.notebookDocument.viewType, this._proxy, webComm.contentProviderComm, document @@ -1339,7 +656,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN } for (const e of this._editors.values()) { - if (e.editor.uri.toString() === revivedUri.toString()) { + if (e.editor.document.uri.toString() === revivedUri.toString()) { e.editor.dispose(); this._editors.delete(e.editor.id); editorChanged = true; @@ -1363,7 +680,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN } const that = this; - const document = new ExtHostNotebookDocument(this._proxy, this._documentsAndEditors, { + const document = new ExtHostNotebookDocument(this._proxy, this._documentsAndEditors, this._mainThreadBulkEdits, { emitModelChange(event: vscode.NotebookCellsChangeEvent): void { that._onDidChangeNotebookCells.fire(event); }, @@ -1375,8 +692,11 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN }, emitCellMetadataChange(event: vscode.NotebookCellMetadataChangeEvent): void { that._onDidChangeCellMetadata.fire(event); + }, + emitDocumentMetadataChange(event: vscode.NotebookDocumentMetadataChangeEvent): void { + that._onDidChangeNotebookDocumentMetadata.fire(event); } - }, viewType, { ...notebookDocumentMetadataDefaults, ...modelData.metadata }, uri, this, storageRoot); + }, viewType, { ...notebookDocumentMetadataDefaults, ...modelData.metadata }, uri, storageRoot); document.acceptModelChanged({ kind: NotebookCellsChangeType.Initialize, @@ -1501,11 +821,6 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN } } -function hashPath(resource: URI): string { - const str = resource.scheme === Schemas.file || resource.scheme === Schemas.untitled ? resource.fsPath : resource.toString(); - return hash(str) + ''; -} - function isEditEvent(e: vscode.NotebookDocumentEditEvent | vscode.NotebookDocumentContentChangeEvent): e is vscode.NotebookDocumentEditEvent { return typeof (e as vscode.NotebookDocumentEditEvent).undo === 'function' && typeof (e as vscode.NotebookDocumentEditEvent).redo === 'function'; diff --git a/src/vs/workbench/api/common/extHostNotebookDocument.ts b/src/vs/workbench/api/common/extHostNotebookDocument.ts new file mode 100644 index 00000000000..ac83b9de093 --- /dev/null +++ b/src/vs/workbench/api/common/extHostNotebookDocument.ts @@ -0,0 +1,521 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Emitter, Event } from 'vs/base/common/event'; +import { hash } from 'vs/base/common/hash'; +import { Disposable, DisposableStore, dispose, IDisposable } from 'vs/base/common/lifecycle'; +import { Schemas } from 'vs/base/common/network'; +import { joinPath } from 'vs/base/common/resources'; +import { ISplice } from 'vs/base/common/sequence'; +import { URI } from 'vs/base/common/uri'; +import * as UUID from 'vs/base/common/uuid'; +import { CellKind, INotebookDocumentPropertiesChangeData, IWorkspaceCellEditDto, MainThreadBulkEditsShape, MainThreadNotebookShape, NotebookCellOutputsSplice, WorkspaceEditType } from 'vs/workbench/api/common/extHost.protocol'; +import { ExtHostDocumentsAndEditors, IExtHostModelAddedData } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; +import { CellEditType, CellOutputKind, diff, IMainCellDto, IProcessedOutput, NotebookCellMetadata, NotebookCellsChangedEventDto, NotebookCellsChangeType, NotebookCellsSplice2, notebookDocumentMetadataDefaults } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import * as vscode from 'vscode'; +import { Cache } from './cache'; + + +interface IObservable { + proxy: T; + onDidChange: Event; +} + +function getObservable(obj: T): IObservable { + const onDidChange = new Emitter(); + const proxy = new Proxy(obj, { + set(target: T, p: PropertyKey, value: any, _receiver: any): boolean { + target[p as keyof T] = value; + onDidChange.fire(); + return true; + } + }); + + return { + proxy, + onDidChange: onDidChange.event + }; +} + +class RawContentChangeEvent { + + constructor(readonly start: number, readonly deletedCount: number, readonly deletedItems: ExtHostCell[], readonly items: ExtHostCell[]) { } + + static asApiEvent(event: RawContentChangeEvent): vscode.NotebookCellsChangeData { + return Object.freeze({ + start: event.start, + deletedCount: event.deletedCount, + deletedItems: event.deletedItems.map(data => data.cell), + items: event.items.map(data => data.cell) + }); + } +} + +export class ExtHostCell extends Disposable { + + static asModelAddData(notebook: vscode.NotebookDocument, cell: IMainCellDto): IExtHostModelAddedData { + return { + EOL: cell.eol, + lines: cell.source, + modeId: cell.language, + uri: cell.uri, + isDirty: false, + versionId: 1, + notebook + }; + } + + private _onDidDispose = new Emitter(); + readonly onDidDispose: Event = this._onDidDispose.event; + + private _onDidChangeOutputs = new Emitter[]>(); + readonly onDidChangeOutputs: Event[]> = this._onDidChangeOutputs.event; + + private _outputs: any[]; + private _outputMapping = new WeakMap(); + + private _metadata: vscode.NotebookCellMetadata; + private _metadataChangeListener: IDisposable; + + readonly handle: number; + readonly uri: URI; + readonly cellKind: CellKind; + + private _cell: vscode.NotebookCell | undefined; + + constructor( + private readonly _mainThreadBulkEdits: MainThreadBulkEditsShape, + private readonly _notebook: ExtHostNotebookDocument, + private readonly _extHostDocument: ExtHostDocumentsAndEditors, + private readonly _cellData: IMainCellDto, + ) { + super(); + + this.handle = _cellData.handle; + this.uri = URI.revive(_cellData.uri); + this.cellKind = _cellData.cellKind; + + this._outputs = _cellData.outputs; + for (const output of this._outputs) { + this._outputMapping.set(output, output.outputId); + delete output.outputId; + } + + const observableMetadata = getObservable(_cellData.metadata ?? {}); + this._metadata = observableMetadata.proxy; + this._metadataChangeListener = this._register(observableMetadata.onDidChange(() => { + this._updateMetadata(); + })); + } + + get cell(): vscode.NotebookCell { + if (!this._cell) { + const that = this; + const document = this._extHostDocument.getDocument(this.uri)!.document; + this._cell = Object.freeze({ + notebook: that._notebook.notebookDocument, + uri: that.uri, + cellKind: this._cellData.cellKind, + document, + language: document.languageId, + get outputs() { return that._outputs; }, + set outputs(value) { that._updateOutputs(value); }, + get metadata() { return that._metadata; }, + set metadata(value) { + that.setMetadata(value); + that._updateMetadata(); + }, + }); + } + return this._cell; + } + + dispose() { + super.dispose(); + this._onDidDispose.fire(); + } + + setOutputs(newOutputs: vscode.CellOutput[]): void { + this._outputs = newOutputs; + } + + private _updateOutputs(newOutputs: vscode.CellOutput[]) { + const rawDiffs = diff(this._outputs || [], newOutputs || [], (a) => { + return this._outputMapping.has(a); + }); + + const transformedDiffs: ISplice[] = rawDiffs.map(diff => { + for (let i = diff.start; i < diff.start + diff.deleteCount; i++) { + this._outputMapping.delete(this._outputs[i]); + } + + return { + deleteCount: diff.deleteCount, + start: diff.start, + toInsert: diff.toInsert.map((output): IProcessedOutput => { + if (output.outputKind === CellOutputKind.Rich) { + const uuid = UUID.generateUuid(); + this._outputMapping.set(output, uuid); + return { ...output, outputId: uuid }; + } + + this._outputMapping.set(output, undefined); + return output; + }) + }; + }); + + this._outputs = newOutputs; + this._onDidChangeOutputs.fire(transformedDiffs); + } + + setMetadata(newMetadata: vscode.NotebookCellMetadata): void { + // Don't apply metadata defaults here, 'undefined' means 'inherit from document metadata' + this._metadataChangeListener.dispose(); + const observableMetadata = getObservable(newMetadata); + this._metadata = observableMetadata.proxy; + this._metadataChangeListener = this._register(observableMetadata.onDidChange(() => { + this._updateMetadata(); + })); + } + + private _updateMetadata(): Promise { + const index = this._notebook.notebookDocument.cells.indexOf(this.cell); + const edit: IWorkspaceCellEditDto = { + _type: WorkspaceEditType.Cell, + metadata: undefined, + resource: this._notebook.uri, + notebookVersionId: this._notebook.notebookDocument.version, + edit: { editType: CellEditType.Metadata, index, metadata: this._metadata } + }; + + return this._mainThreadBulkEdits.$tryApplyWorkspaceEdit({ edits: [edit] }); + } +} + +export interface INotebookEventEmitter { + emitModelChange(events: vscode.NotebookCellsChangeEvent): void; + emitDocumentMetadataChange(event: vscode.NotebookDocumentMetadataChangeEvent): void; + emitCellOutputsChange(event: vscode.NotebookCellOutputsChangeEvent): void; + emitCellLanguageChange(event: vscode.NotebookCellLanguageChangeEvent): void; + emitCellMetadataChange(event: vscode.NotebookCellMetadataChangeEvent): void; +} + +function hashPath(resource: URI): string { + const str = resource.scheme === Schemas.file || resource.scheme === Schemas.untitled ? resource.fsPath : resource.toString(); + return hash(str) + ''; +} + +export class ExtHostNotebookDocument extends Disposable { + + private static _handlePool: number = 0; + readonly handle = ExtHostNotebookDocument._handlePool++; + + private _cells: ExtHostCell[] = []; + + private _cellDisposableMapping = new Map(); + + private _notebook: vscode.NotebookDocument | undefined; + private _metadata: Required; + private _metadataChangeListener: IDisposable; + private _versionId = 0; + private _isDirty: boolean = false; + private _backupCounter = 1; + private _backup?: vscode.NotebookDocumentBackup; + private _disposed = false; + private _languages: string[] = []; + + private readonly _edits = new Cache('notebook documents'); + + constructor( + private readonly _proxy: MainThreadNotebookShape, + private readonly _documentsAndEditors: ExtHostDocumentsAndEditors, + private readonly _mainThreadBulkEdits: MainThreadBulkEditsShape, + private readonly _emitter: INotebookEventEmitter, + private readonly _viewType: string, + metadata: Required, + public readonly uri: URI, + private readonly _storagePath: URI | undefined + ) { + super(); + + const observableMetadata = getObservable(metadata); + this._metadata = observableMetadata.proxy; + this._metadataChangeListener = this._register(observableMetadata.onDidChange(() => { + this._tryUpdateMetadata(); + })); + } + + dispose() { + this._disposed = true; + super.dispose(); + dispose(this._cellDisposableMapping.values()); + } + + private _updateMetadata(newMetadata: Required) { + this._metadataChangeListener.dispose(); + newMetadata = { + ...notebookDocumentMetadataDefaults, + ...newMetadata + }; + if (this._metadataChangeListener) { + this._metadataChangeListener.dispose(); + } + + const observableMetadata = getObservable(newMetadata); + this._metadata = observableMetadata.proxy; + this._metadataChangeListener = this._register(observableMetadata.onDidChange(() => { + this._tryUpdateMetadata(); + })); + + this._tryUpdateMetadata(); + } + + private _tryUpdateMetadata() { + const edit: IWorkspaceCellEditDto = { + _type: WorkspaceEditType.Cell, + metadata: undefined, + notebookMetadata: this._metadata, + resource: this.uri, + notebookVersionId: this.notebookDocument.version, + }; + + return this._mainThreadBulkEdits.$tryApplyWorkspaceEdit({ edits: [edit] }); + } + + get notebookDocument(): vscode.NotebookDocument { + if (!this._notebook) { + const that = this; + this._notebook = Object.freeze({ + get uri() { return that.uri; }, + get version() { return that._versionId; }, + get fileName() { return that.uri.fsPath; }, + get viewType() { return that._viewType; }, + get isDirty() { return that._isDirty; }, + get isUntitled() { return that.uri.scheme === Schemas.untitled; }, + get cells(): ReadonlyArray { return that._cells.map(cell => cell.cell); }, + get languages() { return that._languages; }, + set languages(value: string[]) { that._trySetLanguages(value); }, + get metadata() { return that._metadata; }, + set metadata(value: Required) { that._updateMetadata(value); }, + }); + } + return this._notebook; + } + + private _trySetLanguages(newLanguages: string[]) { + this._languages = newLanguages; + this._proxy.$updateNotebookLanguages(this._viewType, this.uri, this._languages); + } + + getNewBackupUri(): URI { + if (!this._storagePath) { + throw new Error('Backup requires a valid storage path'); + } + const fileName = hashPath(this.uri) + (this._backupCounter++); + return joinPath(this._storagePath, fileName); + } + + updateBackup(backup: vscode.NotebookDocumentBackup): void { + this._backup?.delete(); + this._backup = backup; + } + + disposeBackup(): void { + this._backup?.delete(); + this._backup = undefined; + } + + acceptDocumentPropertiesChanged(data: INotebookDocumentPropertiesChangeData) { + const newMetadata = { + ...notebookDocumentMetadataDefaults, + ...data.metadata + }; + + if (this._metadataChangeListener) { + this._metadataChangeListener.dispose(); + } + + const observableMetadata = getObservable(newMetadata); + this._metadata = observableMetadata.proxy; + this._metadataChangeListener = this._register(observableMetadata.onDidChange(() => { + this._tryUpdateMetadata(); + })); + + this._emitter.emitDocumentMetadataChange({ document: this.notebookDocument }); + } + + acceptModelChanged(event: NotebookCellsChangedEventDto, isDirty: boolean): void { + this._versionId = event.versionId; + this._isDirty = isDirty; + if (event.kind === NotebookCellsChangeType.Initialize) { + this._spliceNotebookCells(event.changes, true); + } if (event.kind === NotebookCellsChangeType.ModelChange) { + this._spliceNotebookCells(event.changes, false); + } else if (event.kind === NotebookCellsChangeType.Move) { + this._moveCell(event.index, event.newIdx); + } else if (event.kind === NotebookCellsChangeType.Output) { + this._setCellOutputs(event.index, event.outputs); + } else if (event.kind === NotebookCellsChangeType.ChangeLanguage) { + this._changeCellLanguage(event.index, event.language); + } else if (event.kind === NotebookCellsChangeType.ChangeCellMetadata) { + this._changeCellMetadata(event.index, event.metadata); + } + } + + private _spliceNotebookCells(splices: NotebookCellsSplice2[], initialization: boolean): void { + if (this._disposed) { + return; + } + + const contentChangeEvents: RawContentChangeEvent[] = []; + const addedCellDocuments: IExtHostModelAddedData[] = []; + const removedCellDocuments: URI[] = []; + + splices.reverse().forEach(splice => { + const cellDtos = splice[2]; + const newCells = cellDtos.map(cell => { + + const extCell = new ExtHostCell(this._mainThreadBulkEdits, this, this._documentsAndEditors, cell); + + if (!initialization) { + addedCellDocuments.push(ExtHostCell.asModelAddData(this.notebookDocument, cell)); + } + + if (!this._cellDisposableMapping.has(extCell.handle)) { + const store = new DisposableStore(); + store.add(extCell); + this._cellDisposableMapping.set(extCell.handle, store); + } + + const store = this._cellDisposableMapping.get(extCell.handle)!; + + store.add(extCell.onDidChangeOutputs((diffs) => { + this.eventuallyUpdateCellOutputs(extCell, diffs); + })); + + return extCell; + }); + + for (let j = splice[0]; j < splice[0] + splice[1]; j++) { + this._cellDisposableMapping.get(this._cells[j].handle)?.dispose(); + this._cellDisposableMapping.delete(this._cells[j].handle); + } + + const deletedItems = this._cells.splice(splice[0], splice[1], ...newCells); + for (let cell of deletedItems) { + removedCellDocuments.push(cell.uri); + } + + contentChangeEvents.push(new RawContentChangeEvent(splice[0], splice[1], deletedItems, newCells)); + }); + + this._documentsAndEditors.acceptDocumentsAndEditorsDelta({ + addedDocuments: addedCellDocuments, + removedDocuments: removedCellDocuments + }); + + if (!initialization) { + this._emitter.emitModelChange({ + document: this.notebookDocument, + changes: contentChangeEvents.map(RawContentChangeEvent.asApiEvent) + }); + } + } + + private _moveCell(index: number, newIdx: number): void { + const cells = this._cells.splice(index, 1); + this._cells.splice(newIdx, 0, ...cells); + const changes: vscode.NotebookCellsChangeData[] = [{ + start: index, + deletedCount: 1, + deletedItems: cells.map(data => data.cell), + items: [] + }, { + start: newIdx, + deletedCount: 0, + deletedItems: [], + items: cells.map(data => data.cell) + }]; + this._emitter.emitModelChange({ + document: this.notebookDocument, + changes + }); + } + + private _setCellOutputs(index: number, outputs: IProcessedOutput[]): void { + const cell = this._cells[index]; + cell.setOutputs(outputs); + this._emitter.emitCellOutputsChange({ document: this.notebookDocument, cells: [cell.cell] }); + } + + private _changeCellLanguage(index: number, language: string): void { + const cell = this._cells[index]; + const event: vscode.NotebookCellLanguageChangeEvent = { document: this.notebookDocument, cell: cell.cell, language }; + this._emitter.emitCellLanguageChange(event); + } + + private _changeCellMetadata(index: number, newMetadata: NotebookCellMetadata | undefined): void { + const cell = this._cells[index]; + cell.setMetadata(newMetadata || {}); + const event: vscode.NotebookCellMetadataChangeEvent = { document: this.notebookDocument, cell: cell.cell }; + this._emitter.emitCellMetadataChange(event); + } + + async eventuallyUpdateCellOutputs(cell: ExtHostCell, diffs: ISplice[]) { + const outputDtos: NotebookCellOutputsSplice[] = diffs.map(diff => { + const outputs = diff.toInsert; + return [diff.start, diff.deleteCount, outputs]; + }); + + if (!outputDtos.length) { + return; + } + + await this._proxy.$spliceNotebookCellOutputs(this._viewType, this.uri, cell.handle, outputDtos); + this._emitter.emitCellOutputsChange({ + document: this.notebookDocument, + cells: [cell.cell] + }); + } + + getCell(cellHandle: number): ExtHostCell | undefined { + return this._cells.find(cell => cell.handle === cellHandle); + } + + + addEdit(item: vscode.NotebookDocumentEditEvent): number { + return this._edits.add([item]); + } + + async undo(editId: number, isDirty: boolean): Promise { + await this.getEdit(editId).undo(); + // if (!isDirty) { + // this.disposeBackup(); + // } + } + + async redo(editId: number, isDirty: boolean): Promise { + await this.getEdit(editId).redo(); + // if (!isDirty) { + // this.disposeBackup(); + // } + } + + private getEdit(editId: number): vscode.NotebookDocumentEditEvent { + const edit = this._edits.get(editId, 0); + if (!edit) { + throw new Error('No edit found'); + } + + return edit; + } + + disposeEdits(editIds: number[]): void { + for (const id of editIds) { + this._edits.delete(id); + } + } +} diff --git a/src/vs/workbench/api/common/extHostNotebookEditor.ts b/src/vs/workbench/api/common/extHostNotebookEditor.ts new file mode 100644 index 00000000000..4b98b86ae66 --- /dev/null +++ b/src/vs/workbench/api/common/extHostNotebookEditor.ts @@ -0,0 +1,257 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { readonly } from 'vs/base/common/errors'; +import { Emitter, Event } from 'vs/base/common/event'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { CellKind, MainThreadNotebookShape } from 'vs/workbench/api/common/extHost.protocol'; +import * as extHostTypes from 'vs/workbench/api/common/extHostTypes'; +import { addIdToOutput, CellEditType, ICellEditOperation, ICellReplaceEdit, INotebookEditData, NotebookDocumentMetadata, notebookDocumentMetadataDefaults } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import * as vscode from 'vscode'; +import { ExtHostNotebookDocument } from './extHostNotebookDocument'; + +class NotebookEditorCellEditBuilder implements vscode.NotebookEditorEdit { + + private readonly _documentVersionId: number; + + private _finalized: boolean = false; + private _collectedEdits: ICellEditOperation[] = []; + private _newNotebookDocumentMetadata?: NotebookDocumentMetadata; + + constructor(documentVersionId: number) { + this._documentVersionId = documentVersionId; + } + + finalize(): INotebookEditData { + this._finalized = true; + return { + documentVersionId: this._documentVersionId, + cellEdits: this._collectedEdits, + newMetadata: this._newNotebookDocumentMetadata + }; + } + + private _throwIfFinalized() { + if (this._finalized) { + throw new Error('Edit is only valid while callback runs'); + } + } + + replaceNotebookMetadata(value: vscode.NotebookDocumentMetadata): void { + this._throwIfFinalized(); + this._newNotebookDocumentMetadata = { ...notebookDocumentMetadataDefaults, ...value }; + } + + replaceCellMetadata(index: number, metadata: vscode.NotebookCellMetadata): void { + this._throwIfFinalized(); + this._collectedEdits.push({ + editType: CellEditType.Metadata, + index, + metadata + }); + } + + replaceMetadata(index: number, metadata: vscode.NotebookCellMetadata): void { + console.warn('DEPRECATED use "replaceCellMetadata" instead'); + this.replaceCellMetadata(index, metadata); + } + + + replaceCellOutput(index: number, outputs: vscode.CellOutput[]): void { + this._throwIfFinalized(); + this._collectedEdits.push({ + editType: CellEditType.Output, + index, + outputs: outputs.map(output => addIdToOutput(output)) + }); + } + + replaceOutput(index: number, outputs: vscode.CellOutput[]): void { + console.warn('DEPRECATED use "replaceCellOutput" instead'); + this.replaceCellOutput(index, outputs); + } + + replaceCells(from: number, to: number, cells: vscode.NotebookCellData[]): void { + this._throwIfFinalized(); + + this._collectedEdits.push({ + editType: CellEditType.Replace, + index: from, + count: to - from, + cells: cells.map(data => { + return { + ...data, + outputs: data.outputs.map(output => addIdToOutput(output)), + }; + }) + }); + } + + insert(index: number, content: string | string[], language: string, type: CellKind, outputs: vscode.CellOutput[], metadata: vscode.NotebookCellMetadata | undefined): void { + this._throwIfFinalized(); + this.replaceCells(index, index, [{ + language, + outputs, + metadata, + cellKind: type, + source: Array.isArray(content) ? content.join('\n') : content, + }]); + } + + delete(index: number): void { + this._throwIfFinalized(); + this.replaceCells(index, 1, []); + } +} + +export class ExtHostNotebookEditor extends Disposable implements vscode.NotebookEditor { + + //TODO@rebornix noop setter? + selection?: vscode.NotebookCell; + + private _visibleRanges: vscode.NotebookCellRange[] = []; + private _viewColumn?: vscode.ViewColumn; + private _active: boolean = false; + private _visible: boolean = false; + private _kernel?: vscode.NotebookKernel; + + private _onDidDispose = new Emitter(); + private _onDidReceiveMessage = new Emitter(); + + readonly onDidDispose: Event = this._onDidDispose.event; + readonly onDidReceiveMessage: vscode.Event = this._onDidReceiveMessage.event; + + constructor( + readonly id: string, + private readonly _viewType: string, + private readonly _proxy: MainThreadNotebookShape, + private readonly _webComm: vscode.NotebookCommunication, + readonly notebookData: ExtHostNotebookDocument, + ) { + super(); + this._register(this._webComm.onDidReceiveMessage(e => { + this._onDidReceiveMessage.fire(e); + })); + } + + get viewColumn(): vscode.ViewColumn | undefined { + return this._viewColumn; + } + + set viewColumn(_value) { + throw readonly('viewColumn'); + } + + get kernel() { + return this._kernel; + } + + set kernel(_kernel: vscode.NotebookKernel | undefined) { + throw readonly('kernel'); + } + + _acceptKernel(kernel?: vscode.NotebookKernel) { + this._kernel = kernel; + } + + get visible(): boolean { + return this._visible; + } + + set visible(_state: boolean) { + throw readonly('visible'); + } + + _acceptVisibility(value: boolean) { + this._visible = value; + } + + get visibleRanges() { + return this._visibleRanges; + } + + set visibleRanges(_range: vscode.NotebookCellRange[]) { + throw readonly('visibleRanges'); + } + + _acceptVisibleRanges(value: vscode.NotebookCellRange[]): void { + this._visibleRanges = value; + } + + get active(): boolean { + return this._active; + } + + set active(_state: boolean) { + throw readonly('active'); + } + + _acceptActive(value: boolean) { + this._active = value; + } + + get document(): vscode.NotebookDocument { + return this.notebookData.notebookDocument; + } + + edit(callback: (editBuilder: NotebookEditorCellEditBuilder) => void): Thenable { + const edit = new NotebookEditorCellEditBuilder(this.document.version); + callback(edit); + return this._applyEdit(edit.finalize()); + } + + private _applyEdit(editData: INotebookEditData): Promise { + + // return when there is nothing to do + if (editData.cellEdits.length === 0) { + return Promise.resolve(true); + } + + const compressedEdits: ICellEditOperation[] = []; + let compressedEditsIndex = -1; + + for (let i = 0; i < editData.cellEdits.length; i++) { + if (compressedEditsIndex < 0) { + compressedEdits.push(editData.cellEdits[i]); + compressedEditsIndex++; + continue; + } + + const prevIndex = compressedEditsIndex; + const prev = compressedEdits[prevIndex]; + + if (prev.editType === CellEditType.Replace && editData.cellEdits[i].editType === CellEditType.Replace) { + const edit = editData.cellEdits[i]; + if (edit.editType !== CellEditType.DocumentMetadata && prev.index === edit.index) { + prev.cells.push(...(editData.cellEdits[i] as ICellReplaceEdit).cells); + prev.count += (editData.cellEdits[i] as ICellReplaceEdit).count; + continue; + } + } + + compressedEdits.push(editData.cellEdits[i]); + compressedEditsIndex++; + } + + return this._proxy.$tryApplyEdits(this._viewType, this.document.uri, editData.documentVersionId, compressedEdits, editData.newMetadata); + } + + revealRange(range: vscode.NotebookCellRange, revealType?: extHostTypes.NotebookEditorRevealType) { + this._proxy.$tryRevealRange(this.id, range, revealType || extHostTypes.NotebookEditorRevealType.Default); + } + + async postMessage(message: any): Promise { + return this._webComm.postMessage(message); + } + + asWebviewUri(localResource: vscode.Uri): vscode.Uri { + return this._webComm.asWebviewUri(localResource); + } + + dispose() { + this._onDidDispose.fire(); + super.dispose(); + } +} diff --git a/src/vs/workbench/api/common/extHostTask.ts b/src/vs/workbench/api/common/extHostTask.ts index 3fb57723234..f0bb1dfacaa 100644 --- a/src/vs/workbench/api/common/extHostTask.ts +++ b/src/vs/workbench/api/common/extHostTask.ts @@ -456,7 +456,7 @@ export abstract class ExtHostTaskBase implements ExtHostTaskShape, IExtHostTask }); } - public abstract async executeTask(extension: IExtensionDescription, task: vscode.Task): Promise; + public abstract executeTask(extension: IExtensionDescription, task: vscode.Task): Promise; public get taskExecutions(): vscode.TaskExecution[] { const result: vscode.TaskExecution[] = []; @@ -561,7 +561,7 @@ export abstract class ExtHostTaskBase implements ExtHostTaskShape, IExtHostTask }); } - protected abstract async resolveTaskInternal(resolvedTaskDTO: tasks.TaskDTO): Promise; + protected abstract resolveTaskInternal(resolvedTaskDTO: tasks.TaskDTO): Promise; public async $resolveTask(handle: number, taskDTO: tasks.TaskDTO): Promise { const handler = this._handlers.get(handle); @@ -601,7 +601,7 @@ export abstract class ExtHostTaskBase implements ExtHostTaskShape, IExtHostTask return await this.resolveTaskInternal(resolvedTaskDTO); } - public abstract async $resolveVariables(uriComponents: UriComponents, toResolve: { process?: { name: string; cwd?: string; path?: string }, variables: string[] }): Promise<{ process?: string, variables: { [key: string]: string; } }>; + public abstract $resolveVariables(uriComponents: UriComponents, toResolve: { process?: { name: string; cwd?: string; path?: string }, variables: string[] }): Promise<{ process?: string, variables: { [key: string]: string; } }>; public abstract $getDefaultShellAndArgs(): Promise<{ shell: string, args: string[] | string | undefined }>; diff --git a/src/vs/workbench/api/common/extHostTerminalService.ts b/src/vs/workbench/api/common/extHostTerminalService.ts index 7107003f8f7..01c4ece2795 100644 --- a/src/vs/workbench/api/common/extHostTerminalService.ts +++ b/src/vs/workbench/api/common/extHostTerminalService.ts @@ -120,7 +120,7 @@ export class ExtHostTerminal extends BaseExtHostTerminal implements vscode.Termi ) { super(proxy, id); this._creationOptions = Object.freeze(this._creationOptions); - this._pidPromise = new Promise(c => this._pidPromiseComplete = c); + this._pidPromise = new Promise(c => this._pidPromiseComplete = c); } public async create( @@ -316,7 +316,7 @@ export abstract class BaseExtHostTerminalService implements IExtHostTerminalServ protected _terminalProcesses: { [id: number]: ITerminalChildProcess } = {}; protected _terminalProcessDisposables: { [id: number]: IDisposable } = {}; protected _extensionTerminalAwaitingStart: { [id: number]: { initialDimensions: ITerminalDimensionsDto | undefined } | undefined } = {}; - protected _getTerminalPromises: { [id: number]: Promise } = {}; + protected _getTerminalPromises: { [id: number]: Promise } = {}; protected _environmentVariableCollections: Map = new Map(); private readonly _bufferer: TerminalDataBufferer; @@ -670,7 +670,7 @@ export abstract class BaseExtHostTerminalService implements IExtHostTerminalServ return this._getTerminalPromises[id]; } - private _createGetTerminalPromise(id: number, retries: number = 5): Promise { + private _createGetTerminalPromise(id: number, retries: number = 5): Promise { return new Promise(c => { if (retries === 0) { c(undefined); diff --git a/src/vs/workbench/api/common/extHostTextEditors.ts b/src/vs/workbench/api/common/extHostTextEditors.ts index a2ba800bf5c..3e95b2f382e 100644 --- a/src/vs/workbench/api/common/extHostTextEditors.ts +++ b/src/vs/workbench/api/common/extHostTextEditors.ts @@ -7,7 +7,6 @@ import { Emitter, Event } from 'vs/base/common/event'; import * as arrays from 'vs/base/common/arrays'; import { ExtHostEditorsShape, IEditorPropertiesChangeData, IMainContext, ITextDocumentShowOptions, ITextEditorPositionData, MainContext, MainThreadTextEditorsShape } from 'vs/workbench/api/common/extHost.protocol'; import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; -import { ExtHostNotebookController } from 'vs/workbench/api/common/extHostNotebook'; import { ExtHostTextEditor, TextEditorDecorationType } from 'vs/workbench/api/common/extHostTextEditor'; import * as TypeConverters from 'vs/workbench/api/common/extHostTypeConverters'; import { TextEditorSelectionChangeKind } from 'vs/workbench/api/common/extHostTypes'; @@ -34,7 +33,6 @@ export class ExtHostEditors implements ExtHostEditorsShape { constructor( mainContext: IMainContext, private readonly _extHostDocumentsAndEditors: ExtHostDocumentsAndEditors, - private readonly _extHostNotebooks: ExtHostNotebookController, ) { this._proxy = mainContext.getProxy(MainContext.MainThreadTextEditors); @@ -92,11 +90,6 @@ export class ExtHostEditors implements ExtHostEditorsShape { return new TextEditorDecorationType(this._proxy, options); } - applyWorkspaceEdit(edit: vscode.WorkspaceEdit): Promise { - const dto = TypeConverters.WorkspaceEdit.from(edit, this._extHostDocumentsAndEditors, this._extHostNotebooks); - return this._proxy.$tryApplyWorkspaceEdit(dto); - } - // --- called from main thread $acceptEditorPropertiesChanged(id: string, data: IEditorPropertiesChangeData): void { diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index e5df10e69f8..da9bd09b20a 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -537,10 +537,11 @@ export namespace WorkspaceEdit { } else if (entry._type === types.FileEditType.Cell) { result.edits.push({ _type: extHostProtocol.WorkspaceEditType.Cell, + metadata: entry.metadata, resource: entry.uri, edit: entry.edit, - metadata: entry.metadata, - modelVersionId: notebooks?.lookupNotebookDocument(entry.uri)?.notebookDocument.version + notebookMetadata: entry.notebookMetadata, + notebookVersionId: notebooks?.lookupNotebookDocument(entry.uri)?.notebookDocument.version }); } } diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index d832779db22..94638701e4f 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -601,7 +601,8 @@ export interface IFileTextEdit { export interface IFileCellEdit { _type: FileEditType.Cell; uri: URI; - edit: ICellEditOperation; + edit?: ICellEditOperation; + notebookMetadata?: vscode.NotebookDocumentMetadata; metadata?: vscode.WorkspaceEditEntryMetadata; } @@ -629,7 +630,11 @@ export class WorkspaceEdit implements vscode.WorkspaceEdit { this._edits.push({ _type: FileEditType.File, from: uri, to: undefined, options, metadata }); } - // --- cell + // --- notebook + + replaceNotebookMetadata(uri: URI, value: vscode.NotebookDocumentMetadata, metadata?: vscode.WorkspaceEditEntryMetadata): void { + this._edits.push({ _type: FileEditType.Cell, metadata, uri, notebookMetadata: value }); + } replaceCells(uri: URI, start: number, end: number, cells: vscode.NotebookCellData[], metadata?: vscode.WorkspaceEditEntryMetadata): void { this._edits.push({ _type: FileEditType.Cell, metadata, uri, edit: { editType: CellEditType.Replace, index: start, count: end - start, cells: cells.map(cell => ({ ...cell, outputs: cell.outputs.map(output => addIdToOutput(output)) })) } }); diff --git a/src/vs/workbench/api/common/extHostWebviewView.ts b/src/vs/workbench/api/common/extHostWebviewView.ts index dcb76f3afd3..5122096832e 100644 --- a/src/vs/workbench/api/common/extHostWebviewView.ts +++ b/src/vs/workbench/api/common/extHostWebviewView.ts @@ -29,12 +29,14 @@ class ExtHostWebviewView extends Disposable implements vscode.WebviewView { handle: extHostProtocol.WebviewHandle, proxy: extHostProtocol.MainThreadWebviewViewsShape, viewType: string, + title: string | undefined, webview: ExtHostWebview, isVisible: boolean, ) { super(); this.#viewType = viewType; + this.#title = title; this.#handle = handle; this.#proxy = proxy; this.#webview = webview; @@ -153,6 +155,7 @@ export class ExtHostWebviewViews implements extHostProtocol.ExtHostWebviewViewsS async $resolveWebviewView( webviewHandle: string, viewType: string, + title: string | undefined, state: any, cancellation: CancellationToken, ): Promise { @@ -164,7 +167,7 @@ export class ExtHostWebviewViews implements extHostProtocol.ExtHostWebviewViewsS const { provider, extension } = entry; const webview = this._extHostWebview.createNewWebview(webviewHandle, { /* todo */ }, extension); - const revivedView = new ExtHostWebviewView(webviewHandle, this._proxy, viewType, webview, true); + const revivedView = new ExtHostWebviewView(webviewHandle, this._proxy, viewType, title, webview, true); this._webviewViews.set(webviewHandle, revivedView); diff --git a/src/vs/workbench/contrib/bulkEdit/browser/bulkCellEdits.ts b/src/vs/workbench/contrib/bulkEdit/browser/bulkCellEdits.ts index 5def1d910c2..6db8054a5f3 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/bulkCellEdits.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/bulkCellEdits.ts @@ -9,7 +9,7 @@ import { URI } from 'vs/base/common/uri'; import { ResourceEdit } from 'vs/editor/browser/services/bulkEditService'; import { WorkspaceEditMetadata } from 'vs/editor/common/modes'; import { IProgress } from 'vs/platform/progress/common/progress'; -import { ICellEditOperation } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellEditType, ICellEditOperation, NotebookDocumentMetadata } 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'; @@ -17,7 +17,8 @@ export class ResourceNotebookCellEdit extends ResourceEdit { constructor( readonly resource: URI, - readonly cellEdit: ICellEditOperation, + readonly cellEdit?: ICellEditOperation, + readonly notebookMetadata?: NotebookDocumentMetadata, readonly versionId?: number, readonly metadata?: WorkspaceEditMetadata ) { @@ -48,10 +49,25 @@ export class BulkCellEdits { // throw new Error(`Notebook '${first.resource}' has changed in the meantime`); // } + const edits: ICellEditOperation[] = []; + let newMetadata: NotebookDocumentMetadata | undefined; + for (let edit of group) { + if (edit.cellEdit) { + edits.push(edit.cellEdit); + } + newMetadata = edit.notebookMetadata ?? newMetadata; + } + + // set metadata + if (newMetadata) { + edits.push({ + editType: CellEditType.DocumentMetadata, + metadata: newMetadata + }); + } // apply edits - const cellEdits = group.map(edit => edit.cellEdit); - this._notebookService.transformEditsOutputs(ref.object.notebook, cellEdits); - ref.object.notebook.applyEdit(ref.object.notebook.versionId, cellEdits, true); + this._notebookService.transformEditsOutputs(ref.object.notebook, edits); + ref.object.notebook.applyEdit(ref.object.notebook.versionId, edits, true); ref.dispose(); this._progress.report(undefined); diff --git a/src/vs/workbench/contrib/debug/browser/debugConfigurationManager.ts b/src/vs/workbench/contrib/debug/browser/debugConfigurationManager.ts index 7cc46462c3f..ee4b7b6ec24 100644 --- a/src/vs/workbench/contrib/debug/browser/debugConfigurationManager.ts +++ b/src/vs/workbench/contrib/debug/browser/debugConfigurationManager.ts @@ -317,7 +317,7 @@ export class ConfigurationManager implements IConfigurationManager { await (launch as Launch).writeConfiguration(config); await this.selectConfiguration(launch, config.name); })); - disposables.add(input.onDidHide(() => { chosenDidCancel = true; resolve(); })); + disposables.add(input.onDidHide(() => { chosenDidCancel = true; resolve(undefined); })); }); const token = new CancellationTokenSource(); diff --git a/src/vs/workbench/contrib/debug/browser/debugViewlet.ts b/src/vs/workbench/contrib/debug/browser/debugViewlet.ts index d32774028e5..9496cfa1898 100644 --- a/src/vs/workbench/contrib/debug/browser/debugViewlet.ts +++ b/src/vs/workbench/contrib/debug/browser/debugViewlet.ts @@ -198,7 +198,7 @@ export class DebugViewPaneContainer extends ViewPaneContainer { if (state === State.Initializing) { this.progressService.withProgress({ location: VIEWLET_ID, }, _progress => { - return new Promise(resolve => this.progressResolve = resolve); + return new Promise(resolve => this.progressResolve = resolve); }); } diff --git a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsActions.test.ts b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsActions.test.ts index 3f1897744af..9cd9d45d85e 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsActions.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsActions.test.ts @@ -469,7 +469,7 @@ suite('ExtensionsActions', () => { testObject.extension = extensions[0]; instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(aGalleryExtension('a', { identifier: local.identifier, version: '1.0.1' }))); assert.ok(!testObject.enabled); - return new Promise(c => { + return new Promise(c => { testObject.onDidChange(() => { if (testObject.enabled) { c(); @@ -1025,7 +1025,7 @@ suite('ExtensionsActions', () => { .then(async () => { instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(aGalleryExtension('a', { identifier: local[0].identifier, version: '1.0.2' }), aGalleryExtension('b', { identifier: local[1].identifier, version: '1.0.2' }), aGalleryExtension('c', local[2].manifest))); assert.ok(!testObject.enabled); - return new Promise(c => { + return new Promise(c => { testObject.onDidChange(() => { if (testObject.enabled) { c(); @@ -1046,7 +1046,7 @@ suite('ExtensionsActions', () => { .then(async () => { instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(...gallery)); assert.ok(!testObject.enabled); - return new Promise(c => { + return new Promise(c => { installEvent.fire({ identifier: local[0].identifier, gallery: gallery[0] }); testObject.onDidChange(() => { if (testObject.enabled) { @@ -1246,7 +1246,7 @@ suite('ReloadAction', () => { const extensions = await workbenchService.queryLocal(); testObject.extension = extensions[0]; - return new Promise(c => { + return new Promise(c => { testObject.onDidChange(() => { if (testObject.enabled && testObject.tooltip === 'Please reload Visual Studio Code to enable the updated extension.') { c(); diff --git a/src/vs/workbench/contrib/files/browser/fileCommands.ts b/src/vs/workbench/contrib/files/browser/fileCommands.ts index cf8f51deedc..66a5bd24f00 100644 --- a/src/vs/workbench/contrib/files/browser/fileCommands.ts +++ b/src/vs/workbench/contrib/files/browser/fileCommands.ts @@ -371,9 +371,14 @@ async function saveSelectedEditors(accessor: ServicesAccessor, options?: ISaveEd // Special treatment for side by side editors: if the active editor // has 2 sides, we consider both, to support saving both sides. - // We only allow this when saving, not for "Save As". + // We only allow this when saving, not for "Save As" and not if any + // editor is untitled which would bring up a "Save As" dialog too. // See also https://github.com/microsoft/vscode/issues/4180 - if (activeGroup.activeEditor instanceof SideBySideEditorInput && !options?.saveAs) { + // See also https://github.com/microsoft/vscode/issues/106330 + if ( + activeGroup.activeEditor instanceof SideBySideEditorInput && + !options?.saveAs && !(activeGroup.activeEditor.primary.isUntitled() || activeGroup.activeEditor.secondary.isUntitled()) + ) { editors.push({ groupId: activeGroup.id, editor: activeGroup.activeEditor.primary }); editors.push({ groupId: activeGroup.id, editor: activeGroup.activeEditor.secondary }); } else { diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts b/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts index 4feefecc161..fc890cbcdad 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts @@ -145,7 +145,7 @@ abstract class NotebookAction extends Action2 { this.runWithContext(accessor, context); } - abstract async runWithContext(accessor: ServicesAccessor, context: INotebookActionContext): Promise; + abstract runWithContext(accessor: ServicesAccessor, context: INotebookActionContext): Promise; private isNotebookActionContext(context?: unknown): context is INotebookActionContext { return !!context && !!(context as INotebookActionContext).notebookEditor; @@ -185,7 +185,7 @@ abstract class NotebookCellAction extends NotebookAction { this.runWithContext(accessor, context); } - abstract async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext): Promise; + abstract runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext): Promise; } registerAction2(class extends NotebookCellAction { @@ -1387,7 +1387,12 @@ export class ChangeCellLanguageAction extends NotebookCellAction { } else if (selection.languageId !== 'markdown' && context.cell?.language === 'markdown') { await changeCellToKind(CellKind.Code, { cell: context.cell, notebookEditor: context.notebookEditor }, selection.languageId); } else { - context.notebookEditor.viewModel!.notebookDocument.changeCellLanguage(context.cell.handle, selection.languageId); + const index = context.notebookEditor.viewModel!.notebookDocument.cells.indexOf(context.cell.model); + context.notebookEditor.viewModel!.notebookDocument.applyEdit( + context.notebookEditor.viewModel!.notebookDocument.versionId, + [{ editType: CellEditType.CellLanguage, index, language: selection.languageId }], + true + ); } } } diff --git a/src/vs/workbench/contrib/notebook/browser/diff/cellComponents.ts b/src/vs/workbench/contrib/notebook/browser/diff/cellComponents.ts index 327c3ee0721..56eea16ad4b 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/cellComponents.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/cellComponents.ts @@ -283,6 +283,14 @@ abstract class AbstractCellRenderer extends Disposable { this._metadataHeaderContainer = DOM.append(this._diffEditorContainer, DOM.$('.metadata-header-container')); this._metadataInfoContainer = DOM.append(this._diffEditorContainer, DOM.$('.metadata-info-container')); + const checkIfModified = (cell: CellDiffViewModel) => { + return cell.type !== 'delete' && cell.type !== 'insert' && hash(this._getFormatedMetadataJSON(cell.original?.metadata || {}, cell.original?.language)) !== hash(this._getFormatedMetadataJSON(cell.modified?.metadata ?? {}, cell.modified?.language)); + }; + + if (checkIfModified(this.cell)) { + this.cell.metadataFoldingState = PropertyFoldingState.Expanded; + } + this._metadataHeader = this.instantiationService.createInstance( PropertyHeader, this.cell, @@ -291,7 +299,7 @@ abstract class AbstractCellRenderer extends Disposable { { updateInfoRendering: this.updateMetadataRendering.bind(this), checkIfModified: (cell) => { - return cell.type !== 'delete' && cell.type !== 'insert' && hash(this._getFormatedMetadataJSON(cell.original?.metadata || {}, cell.original?.language)) !== hash(this._getFormatedMetadataJSON(cell.modified?.metadata ?? {}, cell.modified?.language)); + return checkIfModified(cell); }, getFoldingState: (cell) => { return cell.metadataFoldingState; @@ -311,6 +319,14 @@ abstract class AbstractCellRenderer extends Disposable { this._outputHeaderContainer = DOM.append(this._diffEditorContainer, DOM.$('.output-header-container')); this._outputInfoContainer = DOM.append(this._diffEditorContainer, DOM.$('.output-info-container')); + const checkIfOutputsModified = (cell: CellDiffViewModel) => { + return cell.type !== 'delete' && cell.type !== 'insert' && !this.notebookEditor.textModel!.transientOptions.transientOutputs && cell.type === 'modified' && hash(cell.original?.outputs ?? []) !== hash(cell.modified?.outputs ?? []); + }; + + if (checkIfOutputsModified(this.cell)) { + this.cell.outputFoldingState = PropertyFoldingState.Expanded; + } + this._outputHeader = this.instantiationService.createInstance( PropertyHeader, this.cell, @@ -319,10 +335,10 @@ abstract class AbstractCellRenderer extends Disposable { { updateInfoRendering: this.updateOutputRendering.bind(this), checkIfModified: (cell) => { - return cell.type !== 'delete' && cell.type !== 'insert' && !this.notebookEditor.textModel!.transientOptions.transientOutputs && cell.type === 'modified' && hash(cell.original?.outputs ?? []) !== hash(cell.modified?.outputs ?? []); + return checkIfOutputsModified(cell); }, getFoldingState: (cell) => { - return this.cell.outputFoldingState; + return cell.outputFoldingState; }, updateFoldingState: (cell, state) => { cell.outputFoldingState = state; @@ -449,8 +465,14 @@ abstract class AbstractCellRenderer extends Disposable { } if (newLangauge !== undefined && newLangauge !== this.cell.modified!.language) { - this.notebookEditor.textModel!.changeCellLanguage(this.cell.modified!.handle, newLangauge); + const index = this.notebookEditor.textModel!.cells.indexOf(this.cell.modified!); + this.notebookEditor.textModel!.applyEdit( + this.notebookEditor.textModel!.versionId, + [{ editType: CellEditType.CellLanguage, index, language: newLangauge }], + true + ); } + const index = this.notebookEditor.textModel!.cells.indexOf(this.cell.modified!); if (index < 0) { diff --git a/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffEditor.ts b/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffEditor.ts index 78bcd753b76..e973897c2ee 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffEditor.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffEditor.ts @@ -321,7 +321,7 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD r(); })); - return new Promise(resolve => { r = resolve; }); + return new Promise(resolve => { r = resolve; }); } getDomNode() { diff --git a/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffList.ts b/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffList.ts index 2931710c259..6e664d46aa3 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffList.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffList.ts @@ -221,8 +221,8 @@ export class NotebookTextDiffList extends WorkbenchList imple } const newStyles = content.join('\n'); - if (newStyles !== this.styleElement.innerHTML) { - this.styleElement.innerHTML = newStyles; + if (newStyles !== this.styleElement.textContent) { + this.styleElement.textContent = newStyles; } } } diff --git a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts index 1bbae8210d7..105cfb02576 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts @@ -564,9 +564,9 @@ class NotebookFileTracker implements IWorkbenchContribution { constructor( @INotebookService private readonly _notebookService: INotebookService, @IEditorService private readonly _editorService: IEditorService, - @IWorkingCopyService workingCopyService: IWorkingCopyService, + @IWorkingCopyService private readonly _workingCopyService: IWorkingCopyService, ) { - this._dirtyListener = Event.debounce(workingCopyService.onDidChangeDirty, () => { }, 100)(() => { + this._dirtyListener = Event.debounce(_workingCopyService.onDidChangeDirty, () => { }, 100)(() => { const inputs = this._createMissingNotebookEditors(); this._editorService.openEditors(inputs); }); @@ -580,7 +580,7 @@ class NotebookFileTracker implements IWorkbenchContribution { const result: IResourceEditorInput[] = []; for (const notebook of this._notebookService.getNotebookTextModels()) { - if (notebook.isDirty && !this._editorService.isOpen({ resource: notebook.uri })) { + if (this._workingCopyService.isDirty(notebook.uri.with({ scheme: Schemas.vscodeNotebook })) && !this._editorService.isOpen({ resource: notebook.uri })) { result.push({ resource: notebook.uri, options: { inactive: true, preserveFocus: true, pinned: true } diff --git a/src/vs/workbench/contrib/notebook/browser/notebookDiffEditorInput.ts b/src/vs/workbench/contrib/notebook/browser/notebookDiffEditorInput.ts index 6ab3652eaba..1c1c65468a6 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookDiffEditorInput.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookDiffEditorInput.ts @@ -193,14 +193,14 @@ ${patterns} return; } - async resolve(editorId?: string): Promise { + async resolve(): Promise { if (!await this._notebookService.canResolve(this.viewType!)) { return null; } if (!this._textModel) { - this._textModel = await this._notebookModelResolverService.resolve(this.resource, this.viewType!, editorId); - this._originalTextModel = await this._notebookModelResolverService.resolve(this.originalResource, this.viewType!, editorId); + this._textModel = await this._notebookModelResolverService.resolve(this.resource, this.viewType!); + this._originalTextModel = await this._notebookModelResolverService.resolve(this.originalResource, this.viewType!); } return new NotebookDiffEditorModel(this._originalTextModel!.object as NotebookEditorModel, this._textModel.object as NotebookEditorModel); diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts index 03021e5ba4d..624eff243f0 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts @@ -25,6 +25,7 @@ import { IEditorDropService } from 'vs/workbench/services/editor/browser/editorD import { IEditorGroup, IEditorGroupsService, GroupsOrder } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { NotebookEditorOptions } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; const NOTEBOOK_EDITOR_VIEW_STATE_PREFERENCE_KEY = 'NotebookEditorViewState'; @@ -54,6 +55,7 @@ export class NotebookEditor extends EditorPane { @IEditorGroupsService private readonly _editorGroupService: IEditorGroupsService, @IEditorDropService private readonly _editorDropService: IEditorDropService, @INotificationService private readonly _notificationService: INotificationService, + @INotebookService private readonly _notebookService: INotebookService, @INotebookEditorWidgetService private readonly _notebookWidgetService: INotebookEditorWidgetService, ) { super(NotebookEditor.ID, telemetryService, themeService, storageService); @@ -152,7 +154,7 @@ export class NotebookEditor extends EditorPane { this._widget.value!.layout(this._dimension, this._rootElement); } - const model = await input.resolve(this._widget.value!.getId()); + const model = await input.resolve(); // Check for cancellation if (token.isCancellationRequested) { return undefined; @@ -174,6 +176,8 @@ export class NotebookEditor extends EditorPane { return; } + await this._notebookService.resolveNotebookEditor(model.viewType, model.resource, this._widget.value!.getId()); + const viewState = this._loadNotebookEditorViewState(input); await this._widget.value!.setModel(model.notebook, viewState); diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorInput.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorInput.ts index 1df2b996c03..9e11a1b57c5 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorInput.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorInput.ts @@ -168,13 +168,13 @@ ${patterns} return; } - async resolve(editorId?: string): Promise { + async resolve(): Promise { if (!await this._notebookService.canResolve(this.viewType!)) { return null; } if (!this._textModel) { - this._textModel = await this._notebookModelResolverService.resolve(this.resource, this.viewType!, editorId); + this._textModel = await this._notebookModelResolverService.resolve(this.resource, this.viewType!); this._register(this._textModel.object.onDidChangeDirty(() => { this._onDidChangeDirty.fire(); diff --git a/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts index 28b08f6d08d..92da7336506 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts @@ -9,6 +9,7 @@ import { Emitter, Event } from 'vs/base/common/event'; import { Iterable } from 'vs/base/common/iterator'; import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { ResourceMap } from 'vs/base/common/map'; +import { Schemas } from 'vs/base/common/network'; import { URI } from 'vs/base/common/uri'; import { RedoCommand, UndoCommand } from 'vs/editor/browser/editorExtensions'; import { CopyAction, CutAction, PasteAction } from 'vs/editor/contrib/clipboard/clipboard'; @@ -26,7 +27,7 @@ import { NotebookKernelProviderAssociationRegistry, NotebookViewTypesExtensionRe import { CellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; -import { ACCESSIBLE_NOTEBOOK_DISPLAY_ORDER, BUILTIN_RENDERER_ID, CellEditType, CellOutputKind, CellUri, DisplayOrderKey, ICellEditOperation, IDisplayOutput, INotebookKernelInfo2, INotebookKernelProvider, INotebookRendererInfo, INotebookTextModel, IOrderedMimeType, ITransformedDisplayOutputDto, mimeTypeSupportedByCore, NotebookCellOutputsSplice, notebookDocumentFilterMatch, NotebookEditorPriority, NOTEBOOK_DISPLAY_ORDER, sortMimeTypes } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { ACCESSIBLE_NOTEBOOK_DISPLAY_ORDER, BUILTIN_RENDERER_ID, CellEditType, CellKind, CellOutputKind, CellUri, DisplayOrderKey, ICellEditOperation, IDisplayOutput, INotebookKernelInfo2, INotebookKernelProvider, INotebookRendererInfo, INotebookTextModel, IOrderedMimeType, ITransformedDisplayOutputDto, mimeTypeSupportedByCore, NotebookCellOutputsSplice, notebookDocumentFilterMatch, NotebookEditorPriority, NOTEBOOK_DISPLAY_ORDER, sortMimeTypes } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { NotebookOutputRendererInfo } from 'vs/workbench/contrib/notebook/common/notebookOutputRenderer'; import { NotebookEditorDescriptor, NotebookProviderInfo } from 'vs/workbench/contrib/notebook/common/notebookProvider'; import { IMainNotebookController, INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; @@ -605,7 +606,7 @@ export class NotebookService extends Disposable implements INotebookService, ICu return this.notebookRenderersInfoStore.get(id); } - async resolveNotebook(viewType: string, uri: URI, forceReload: boolean, editorId?: string, backupId?: string): Promise { + async resolveNotebook(viewType: string, uri: URI, forceReload: boolean, backupId?: string): Promise { if (!await this.canResolve(viewType)) { throw new Error(`CANNOT load notebook, no provider for '${viewType}'`); @@ -622,8 +623,16 @@ export class NotebookService extends Disposable implements INotebookService, ICu return notebookModel; } else { - notebookModel = this._instantiationService.createInstance(NotebookTextModel, viewType, provider.controller.supportBackup, uri); - await provider.controller.createNotebook(notebookModel, backupId); + const dataDto = await provider.controller.resolveNotebookDocument(viewType, uri, backupId); + let cells = dataDto.data.cells.length ? dataDto.data.cells : (uri.scheme === Schemas.untitled ? [{ + cellKind: CellKind.Code, + language: dataDto.data.languages.length ? dataDto.data.languages[0] : '', + outputs: [], + metadata: undefined, + source: '' + }] : []); + + notebookModel = this._instantiationService.createInstance(NotebookTextModel, viewType, provider.controller.supportBackup, uri, cells, dataDto.data.languages, dataDto.data.metadata, dataDto.transientOptions); } // new notebook model created @@ -637,10 +646,6 @@ export class NotebookService extends Disposable implements INotebookService, ICu // after the document is added to the store and sent to ext host, we transform the ouputs await this.transformTextModelOutputs(notebookModel); - if (editorId) { - await provider.controller.resolveNotebookEditor(viewType, uri, editorId); - } - return modelData.model; } @@ -786,6 +791,13 @@ export class NotebookService extends Disposable implements INotebookService, ICu return ret; } + async resolveNotebookEditor(viewType: string, uri: URI, editorId: string): Promise { + const entry = this._notebookProviders.get(viewType); + if (entry) { + entry.controller.resolveNotebookEditor(viewType, uri, editorId); + } + } + removeNotebookEditor(editor: INotebookEditor) { const editorCache = this._notebookEditors.get(editor.getId()); @@ -919,19 +931,12 @@ export class NotebookService extends Disposable implements INotebookService, ICu } }); + modelData.model.dispose(); + modelData.dispose(); + willRemovedEditors.forEach(e => this._notebookEditors.delete(e.getId())); - - const provider = this._notebookProviders.get(modelData!.model.viewType); - - if (provider) { - provider.controller.removeNotebookDocument(modelData!.model.uri); - modelData!.model.dispose(); - } - - this._onNotebookEditorsRemove.fire(willRemovedEditors.map(e => e)); this._onDidRemoveNotebookDocument.fire(modelData.model.uri); - modelData.dispose(); } } } diff --git a/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts b/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts index f9846096655..c2cce19e6e5 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts @@ -760,7 +760,7 @@ export class NotebookCellList extends WorkbenchList implements ID upwards = true; } - const editorAttachedPromise = new Promise((resolve, reject) => { + const editorAttachedPromise = new Promise((resolve, reject) => { element.onDidChangeEditorAttachState(() => { element.editorAttached ? resolve() : reject(); }); @@ -1032,8 +1032,8 @@ export class NotebookCellList extends WorkbenchList implements ID } const newStyles = content.join('\n'); - if (newStyles !== this.styleElement.innerHTML) { - this.styleElement.innerHTML = newStyles; + if (newStyles !== this.styleElement.textContent) { + this.styleElement.textContent = newStyles; } } @@ -1052,7 +1052,7 @@ export class NotebookCellList extends WorkbenchList implements ID } function getEditorAttachedPromise(element: CellViewModel) { - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { Event.once(element.onDidChangeEditorAttachState)(() => element.editorAttached ? resolve() : reject()); }); } 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 fcffc8ee0fd..c388e390c67 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts @@ -1021,6 +1021,8 @@ export class ListTopCellToolbar extends Disposable { this._modelDisposables.add(this.notebookEditor.viewModel.onDidChangeViewCells(() => { this.updateClass(); })); + + this.updateClass(); } })); diff --git a/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts b/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts index 5537f5d9f15..132928efb9c 100644 --- a/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts +++ b/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts @@ -111,24 +111,14 @@ class DelayedEmitter { } - emit(data: { - triggerDirty: { value: boolean } | undefined, - modelContentChange: { value: NotebookTextModelChangedEvent } | undefined, - }) { + emit(data: NotebookTextModelChangedEvent) { this._increaseVersion(); - - if (data.triggerDirty) { - this._textModel.setDirty(data.triggerDirty.value); - } - - if (data.modelContentChange) { - this._onDidChangeContent.fire( - { - ...data.modelContentChange.value, - versionId: this._textModel.versionId - } - ); - } + this._onDidChangeContent.fire( + { + ...data, + versionId: this._textModel.versionId + } + ); } } @@ -136,17 +126,12 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel private readonly _onWillDispose: Emitter = this._register(new Emitter()); private readonly _onDidChangeContent = this._register(new Emitter()); - private readonly _onDidChangeMetadata = this._register(new Emitter()); - readonly onWillDispose: Event = this._onWillDispose.event; readonly onDidChangeContent = this._onDidChangeContent.event; - readonly onDidChangeMetadata = this._onDidChangeMetadata.event; - private _cellhandlePool: number = 0; private _mapping: Map = new Map(); private _cellListeners: Map = new Map(); - private _cells: NotebookCellTextModel[]; - + private _cells: NotebookCellTextModel[] = []; private _languages: string[] = []; private _allLanguages: boolean = false; @@ -164,52 +149,10 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel metadata: NotebookDocumentMetadata = notebookDocumentMetadataDefaults; transientOptions: TransientOptions = { transientMetadata: {}, transientOutputs: false }; - private _isUntitled: boolean | undefined = undefined; private _versionId = 0; - - //#region selection TODO@rebornix this shouldn't be here - - private readonly _emitSelections = this._register(new Emitter()); - readonly emitSelections = this._emitSelections.event; - private _onDidSelectionChangeProxy = this._register(new Emitter()); - readonly onDidSelectionChange = this._onDidSelectionChangeProxy.event; - - private _operationManager: NotebookOperationManager; private _eventEmitter: DelayedEmitter; - private _dirty = false; - protected readonly _onDidChangeDirty = this._register(new Emitter()); - readonly onDidChangeDirty = this._onDidChangeDirty.event; - - - - constructor( - readonly viewType: string, - readonly supportBackup: boolean, - readonly uri: URI, - @IUndoRedoService private _undoService: IUndoRedoService, - @ITextModelService private _modelService: ITextModelService, - @IModeService private readonly _modeService: IModeService, - ) { - super(); - this._cells = []; - - this._operationManager = new NotebookOperationManager(this._undoService, uri); - this._eventEmitter = new DelayedEmitter( - this._onDidChangeContent, - () => { this._increaseVersionId(); }, - this - ); - } - - dispose() { - this._onWillDispose.fire(); - dispose(this._cellListeners.values()); - dispose(this._cells); - super.dispose(); - } - get cells(): readonly NotebookCellTextModel[] { return this._cells; } @@ -218,15 +161,60 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel return this._versionId; } - get isDirty() { - return this._dirty; + constructor( + readonly viewType: string, + readonly supportBackup: boolean, + readonly uri: URI, + cells: ICellDto2[], + languages: string[], + metadata: NotebookDocumentMetadata, + options: TransientOptions, + @IUndoRedoService private _undoService: IUndoRedoService, + @ITextModelService private _modelService: ITextModelService, + @IModeService private readonly _modeService: IModeService, + ) { + super(); + this.transientOptions = options; + this.metadata = metadata; + this.updateLanguages(languages); + this._initialize(cells); + + this._operationManager = new NotebookOperationManager(this._undoService, uri); + this._eventEmitter = new DelayedEmitter( + this._onDidChangeContent, + () => { this._increaseVersionId(); }, + this + ); } - setDirty(newState: boolean) { - if (this._dirty !== newState) { - this._dirty = newState; - this._onDidChangeDirty.fire(); + private _initialize(cells: ICellDto2[]) { + this._cells = []; + this._versionId = 0; + + const mainCells = cells.map(cell => { + const cellHandle = this._cellhandlePool++; + const cellUri = CellUri.generate(this.uri, cellHandle); + return new NotebookCellTextModel(cellUri, cellHandle, cell.source, cell.language, cell.cellKind, cell.outputs || [], cell.metadata, this.transientOptions, this._modelService); + }); + + for (let i = 0; i < mainCells.length; i++) { + this._mapping.set(mainCells[i].handle, mainCells[i]); + const dirtyStateListener = mainCells[i].onDidChangeContent(() => { + this._eventEmitter.emit({ kind: NotebookCellsChangeType.ChangeCellContent, versionId: this.versionId, synchronous: true, transient: false }); + }); + + this._cellListeners.set(mainCells[i].handle, dirtyStateListener); } + + this._cells.splice(0, 0, ...mainCells); + this._increaseVersionId(); + } + + dispose() { + this._onWillDispose.fire(); + dispose(this._cellListeners.values()); + dispose(this._cells); + super.dispose(); } createCellTextModel( @@ -241,33 +229,6 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel return new NotebookCellTextModel(cellUri, cellHandle, source, language, cellKind, outputs || [], metadata || {}, this.transientOptions, this._modelService); } - initialize(cells: ICellDto2[]) { - this._cells = []; - this._versionId = 0; - - const mainCells = cells.map(cell => { - const cellHandle = this._cellhandlePool++; - const cellUri = CellUri.generate(this.uri, cellHandle); - return new NotebookCellTextModel(cellUri, cellHandle, cell.source, cell.language, cell.cellKind, cell.outputs || [], cell.metadata, this.transientOptions, this._modelService); - }); - - this._isUntitled = false; - - for (let i = 0; i < mainCells.length; i++) { - this._mapping.set(mainCells[i].handle, mainCells[i]); - const dirtyStateListener = mainCells[i].onDidChangeContent(() => { - this.setDirty(true); - this._increaseVersionId(); - this._onDidChangeContent.fire({ kind: NotebookCellsChangeType.ChangeCellContent, versionId: this.versionId, synchronous: true }); - }); - - this._cellListeners.set(mainCells[i].handle, dirtyStateListener); - } - - this._cells.splice(0, 0, ...mainCells); - this._increaseVersionId(); - } - pushStackElement(label: string) { this._operationManager.pushStackElement(label); } @@ -290,10 +251,21 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel const edits = rawEdits.map((edit, index) => { return { edit, - end: edit.editType === CellEditType.Replace ? edit.index + edit.count : edit.index, + end: + edit.editType === CellEditType.DocumentMetadata + ? undefined + : (edit.editType === CellEditType.Replace ? edit.index + edit.count : edit.index), originalIndex: index, }; }).sort((a, b) => { + if (a.end === undefined) { + return -1; + } + + if (b.end === undefined) { + return -1; + } + return b.end - a.end || b.originalIndex - a.originalIndex; }); @@ -313,19 +285,60 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel this._assertIndex(edit.index); this._changeCellMetadata(this._cells[edit.index].handle, edit.metadata, true); break; + case CellEditType.CellLanguage: + this._assertIndex(edit.index); + this._changeCellLanguage(this._cells[edit.index].handle, edit.language); + break; + case CellEditType.DocumentMetadata: + this._updateNotebookMetadata(edit.metadata); + break; } } return true; } + + handleUnknownEdit(label: string | undefined, undo: () => void, redo: () => void): void { + this._operationManager.pushEditOperation({ + type: UndoRedoElementType.Resource, + resource: this.uri, + label: label ?? nls.localize('defaultEditLabel', "Edit"), + undo: async () => { + undo(); + }, + redo: async () => { + redo(); + }, + }); + + this._eventEmitter.emit({ + kind: NotebookCellsChangeType.Unknown, + transient: false, + synchronous: true, + versionId: this._versionId, + }); + } + + handleUnknownChange() { + this._eventEmitter.emit({ + kind: NotebookCellsChangeType.Unknown, + transient: false, + synchronous: true, + versionId: this._versionId, + }); + } + + createSnapshot(preserveBOM?: boolean): ITextSnapshot { + return new NotebookTextModelSnapshot(this); + } + private _replaceCells(index: number, count: number, cellDtos: ICellDto2[], synchronous: boolean): void { if (count === 0 && cellDtos.length === 0) { return; } - this._isUntitled = false; //TODO@rebornix fishy? const oldViewCells = this._cells.slice(0); const oldMap = new Map(this._mapping); @@ -346,10 +359,7 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel this._modelService ); const dirtyStateListener = cell.onDidChangeContent(() => { - this._eventEmitter.emit({ - triggerDirty: { value: true }, - modelContentChange: { value: { kind: NotebookCellsChangeType.ChangeCellContent, versionId: this.versionId, synchronous: true } } - }); + this._eventEmitter.emit({ kind: NotebookCellsChangeType.ChangeCellContent, versionId: this.versionId, synchronous: true, transient: false }); }); this._cellListeners.set(cell.handle, dirtyStateListener); this._mapping.set(cell.handle, cell); @@ -377,125 +387,39 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel // should be deferred this._eventEmitter.emit({ - triggerDirty: { value: true }, - modelContentChange: { - value: { - kind: NotebookCellsChangeType.ModelChange, - versionId: this._versionId, - changes: diffs, - synchronous - } - } + kind: NotebookCellsChangeType.ModelChange, + versionId: this._versionId, + changes: diffs, + synchronous, + transient: false }); } - handleEdit(label: string | undefined, undo: () => void, redo: () => void): void { - this._operationManager.pushEditOperation({ - type: UndoRedoElementType.Resource, - resource: this.uri, - label: label ?? nls.localize('defaultEditLabel', "Edit"), - undo: async () => { - undo(); - }, - redo: async () => { - redo(); - }, - }); - this.setDirty(true); - } - - createSnapshot(preserveBOM?: boolean): ITextSnapshot { - return new NotebookTextModelSnapshot(this); - } - private _increaseVersionId(): void { this._versionId = this._versionId + 1; } - handleUnknownChange() { - this.setDirty(true); - } - updateLanguages(languages: string[]) { const allLanguages = languages.find(lan => lan === '*'); this._allLanguages = allLanguages !== undefined; this._languages = languages; const resolvedLanguages = this.resolvedLanguages; - if (this._isUntitled && resolvedLanguages.length && this._cells.length) { + if (resolvedLanguages.length && this._cells.length) { this._cells[0].language = resolvedLanguages[0]; } } - updateNotebookMetadata(metadata: NotebookDocumentMetadata) { + private _updateNotebookMetadata(metadata: NotebookDocumentMetadata) { this.metadata = metadata; - this._eventEmitter.emit({ - triggerDirty: { value: true }, - modelContentChange: { value: { kind: NotebookCellsChangeType.ChangeDocumentMetadata, versionId: this.versionId, metadata: this.metadata, synchronous: true } } - }); - } - - insertTemplateCell(cell: NotebookCellTextModel) { - if (this._cells.length > 0 || this._isUntitled !== undefined) { - return; - } - - this._isUntitled = true; - this._cells = [cell]; - this._mapping.set(cell.handle, cell); - - const dirtyStateListener = cell.onDidChangeContent(() => { - this._isUntitled = false; - this._eventEmitter.emit({ - triggerDirty: { value: true }, - modelContentChange: { value: { kind: NotebookCellsChangeType.ChangeCellContent, versionId: this.versionId, synchronous: true } } - }); - }); - - this._cellListeners.set(cell.handle, dirtyStateListener); - this.setDirty(false); - - // this._eventEmitter.emit({ - // triggerDirty: undefined, - // modelContentChange: { value: { - // kind: NotebookCellsChangeType.ModelChange, - // versionId: this._versionId, changes: - // [[ - // 0, - // 0, - // [cell] - // ]], - // synchronous: true - // } } - // }); - - this.setDirty(false); - // this._onDidChangeContent.fire(NotebookCellsChangeType.ModelChange); - - this._onDidChangeContent.fire({ - kind: NotebookCellsChangeType.ModelChange, - versionId: this._versionId, changes: - [[ - 0, - 0, - [cell] - ]], - synchronous: true - }); - - return; + this._eventEmitter.emit({ kind: NotebookCellsChangeType.ChangeDocumentMetadata, versionId: this.versionId, metadata: this.metadata, synchronous: true, transient: false }); } private _insertNewCell(index: number, cells: NotebookCellTextModel[], synchronous: boolean, endSelections?: number[]): void { - this._isUntitled = false; - for (let i = 0; i < cells.length; i++) { this._mapping.set(cells[i].handle, cells[i]); const dirtyStateListener = cells[i].onDidChangeContent(() => { - this._eventEmitter.emit({ - triggerDirty: { value: true }, - modelContentChange: { value: { kind: NotebookCellsChangeType.ChangeCellContent, versionId: this.versionId, synchronous: true } } - }); + this._eventEmitter.emit({ kind: NotebookCellsChangeType.ChangeCellContent, versionId: this.versionId, synchronous: true, transient: false }); }); this._cellListeners.set(cells[i].handle, dirtyStateListener); @@ -503,38 +427,29 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel this._cells.splice(index, 0, ...cells); this._eventEmitter.emit({ - triggerDirty: { value: true }, - modelContentChange: { - value: { - kind: NotebookCellsChangeType.ModelChange, - versionId: this._versionId, changes: - [[ - index, - 0, - cells - ]], - synchronous, - endSelections: endSelections - } - } + kind: NotebookCellsChangeType.ModelChange, + versionId: this._versionId, changes: + [[ + index, + 0, + cells + ]], + synchronous, + endSelections: endSelections, + transient: false }); return; } private _removeCell(index: number, count: number, synchronous: boolean, endSelections?: number[]) { - this._isUntitled = false; - for (let i = index; i < index + count; i++) { const cell = this._cells[i]; this._cellListeners.get(cell.handle)?.dispose(); this._cellListeners.delete(cell.handle); } this._cells.splice(index, count); - this._eventEmitter.emit({ - triggerDirty: { value: true }, - modelContentChange: { value: { kind: NotebookCellsChangeType.ModelChange, versionId: this._versionId, changes: [[index, count, []]], synchronous, endSelections } } - }); + this._eventEmitter.emit({ kind: NotebookCellsChangeType.ModelChange, versionId: this._versionId, changes: [[index, count, []]], synchronous, endSelections, transient: false }); } private _isCellMetadataChanged(a: NotebookCellMetadata, b: NotebookCellMetadata) { @@ -580,12 +495,16 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel cell.metadata = metadata; } - this._eventEmitter.emit({ - triggerDirty: { value: triggerDirtyChange }, - modelContentChange: { - value: { kind: NotebookCellsChangeType.ChangeCellMetadata, versionId: this._versionId, index: this._cells.indexOf(cell), metadata: cell.metadata, synchronous: true } - } - }); + this._eventEmitter.emit({ kind: NotebookCellsChangeType.ChangeCellMetadata, versionId: this._versionId, index: this._cells.indexOf(cell), metadata: cell.metadata, synchronous: true, transient: !triggerDirtyChange }); + } + + private _changeCellLanguage(handle: number, languageId: string) { + const cell = this._mapping.get(handle); + if (cell && cell.language !== languageId) { + cell.language = languageId; + + this._eventEmitter.emit({ kind: NotebookCellsChangeType.ChangeLanguage, versionId: this._versionId, index: this._cells.indexOf(cell), language: languageId, synchronous: true, transient: false }); + } } // TODO@rebornix, once adopted the new Edit API in ext host, the method should be private. @@ -595,20 +514,14 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel cell.spliceNotebookCellOutputs(splices); this._eventEmitter.emit({ - triggerDirty: { value: !this.transientOptions.transientOutputs }, - modelContentChange: { - value: { - kind: NotebookCellsChangeType.Output, - versionId: this.versionId, - index: this._cells.indexOf(cell), - outputs: cell.outputs ?? [], - transient: this.transientOptions.transientOutputs, - synchronous: true - } - } + kind: NotebookCellsChangeType.Output, + versionId: this.versionId, + index: this._cells.indexOf(cell), + outputs: cell.outputs ?? [], + transient: this.transientOptions.transientOutputs, + synchronous: true }); } - } private _assertIndex(index: number) { @@ -619,18 +532,6 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel //#region Notebook Text Model Edit API - changeCellLanguage(handle: number, languageId: string) { - const cell = this._mapping.get(handle); - if (cell && cell.language !== languageId) { - cell.language = languageId; - - this._eventEmitter.emit({ - triggerDirty: undefined, - modelContentChange: { value: { kind: NotebookCellsChangeType.ChangeLanguage, versionId: this._versionId, index: this._cells.indexOf(cell), language: languageId, synchronous: true } } - }); - } - } - insertCell(index: number, cell: NotebookCellTextModel, synchronous: boolean, pushUndoStop: boolean, beforeSelections: number[] | undefined, endSelections: number[] | undefined): void { if (pushUndoStop) { this._operationManager.pushEditOperation(new InsertCellEdit(this.uri, index, cell, { @@ -666,14 +567,9 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel this._assertIndex(index); this._assertIndex(newIdx); - { - const cells = this._cells.splice(index, length); - this._cells.splice(newIdx, 0, ...cells); - this._eventEmitter.emit({ - triggerDirty: { value: true }, - modelContentChange: { value: { kind: NotebookCellsChangeType.Move, versionId: this._versionId, index, length, newIdx, cells, synchronous, endSelections } } - }); - } + const cells = this._cells.splice(index, length); + this._cells.splice(newIdx, 0, ...cells); + this._eventEmitter.emit({ kind: NotebookCellsChangeType.Move, versionId: this._versionId, index, length, newIdx, cells, synchronous, endSelections, transient: false }); return true; } diff --git a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts index 75b8abd0a88..d409aacb92e 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts @@ -348,7 +348,8 @@ export enum NotebookCellsChangeType { ChangeCellMetadata = 7, Output = 8, ChangeCellContent = 9, - ChangeDocumentMetadata = 10 + ChangeDocumentMetadata = 10, + Unknown = 11 } export interface NotebookCellsInitializeEvent { @@ -405,13 +406,20 @@ export interface NotebookDocumentChangeMetadataEvent { readonly metadata: NotebookDocumentMetadata | undefined; } -export type NotebookCellsChangedEventDto = NotebookCellsInitializeEvent | NotebookDocumentChangeMetadataEvent | NotebookCellContentChangeEvent | NotebookCellsModelChangedEvent | NotebookCellsModelMoveEvent | NotebookOutputChangedEvent | NotebookCellsChangeLanguageEvent | NotebookCellsChangeMetadataEvent; -export type NotebookTextModelChangedEvent = (NotebookCellsInitializeEvent | NotebookDocumentChangeMetadataEvent | NotebookCellContentChangeEvent | NotebookCellsModelChangedEvent | NotebookCellsModelMoveEvent | NotebookOutputChangedEvent | NotebookCellsChangeLanguageEvent | NotebookCellsChangeMetadataEvent) & { synchronous: boolean; endSelections?: number[] }; +export interface NotebookDocumentUnknownChangeEvent { + readonly kind: NotebookCellsChangeType.Unknown; + readonly versionId: number; +} + +export type NotebookCellsChangedEventDto = NotebookCellsInitializeEvent | NotebookDocumentChangeMetadataEvent | NotebookCellContentChangeEvent | NotebookCellsModelChangedEvent | NotebookCellsModelMoveEvent | NotebookOutputChangedEvent | NotebookCellsChangeLanguageEvent | NotebookCellsChangeMetadataEvent | NotebookDocumentUnknownChangeEvent; +export type NotebookTextModelChangedEvent = (NotebookCellsInitializeEvent | NotebookDocumentChangeMetadataEvent | NotebookCellContentChangeEvent | NotebookCellsModelChangedEvent | NotebookCellsModelMoveEvent | NotebookOutputChangedEvent | NotebookCellsChangeLanguageEvent | NotebookCellsChangeMetadataEvent | NotebookDocumentUnknownChangeEvent) & { synchronous: boolean; endSelections?: number[]; transient: boolean }; export const enum CellEditType { Replace = 1, Output = 2, Metadata = 3, + CellLanguage = 4, + DocumentMetadata = 5, } export interface ICellDto2 { @@ -441,11 +449,24 @@ export interface ICellMetadataEdit { metadata: NotebookCellMetadata; } -export type ICellEditOperation = ICellReplaceEdit | ICellOutputEdit | ICellMetadataEdit; + +export interface ICellLanguageEdit { + editType: CellEditType.CellLanguage; + index: number; + language: string; +} + +export interface IDocumentMetadataEdit { + editType: CellEditType.DocumentMetadata; + metadata: NotebookDocumentMetadata; +} + +export type ICellEditOperation = ICellReplaceEdit | ICellOutputEdit | ICellMetadataEdit | ICellLanguageEdit | IDocumentMetadataEdit; export interface INotebookEditData { documentVersionId: number; - edits: ICellEditOperation[]; + cellEdits: ICellEditOperation[]; + newMetadata?: NotebookDocumentMetadata; } export interface NotebookDataDto { diff --git a/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts b/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts index bc6ee9afeb8..3520b6d5ee1 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts @@ -24,30 +24,25 @@ export interface INotebookLoadOptions { * Go to disk bypassing any cache of the model if any. */ forceReadFromDisk?: boolean; - - editorId?: string; } export class NotebookEditorModel extends EditorModel implements INotebookEditorModel { - protected readonly _onDidChangeDirty = this._register(new Emitter()); - readonly onDidChangeDirty = this._onDidChangeDirty.event; + + private readonly _onDidChangeDirty = this._register(new Emitter()); private readonly _onDidChangeContent = this._register(new Emitter()); - readonly onDidChangeContent: Event = this._onDidChangeContent.event; + + readonly onDidChangeDirty = this._onDidChangeDirty.event; + readonly onDidChangeContent = this._onDidChangeContent.event; + private _notebook!: NotebookTextModel; - private _lastResolvedFileStat: IFileStatWithMetadata | undefined; - - get lastResolvedFileStat() { - return this._lastResolvedFileStat; - } - - get notebook() { - return this._notebook; - } + private _lastResolvedFileStat?: IFileStatWithMetadata; private readonly _name: string; private readonly _workingCopyResource: URI; + private _dirty = false; + constructor( readonly resource: URI, readonly viewType: string, @@ -62,23 +57,38 @@ export class NotebookEditorModel extends EditorModel implements INotebookEditorM this._name = labelService.getUriBasenameLabel(resource); - const input = this; + const that = this; this._workingCopyResource = resource.with({ scheme: Schemas.vscodeNotebook }); const workingCopyAdapter = new class implements IWorkingCopy { - readonly resource = input._workingCopyResource; - get name() { return input._name; } - readonly capabilities = input.isUntitled() ? WorkingCopyCapabilities.Untitled : WorkingCopyCapabilities.None; - readonly onDidChangeDirty = input.onDidChangeDirty; - readonly onDidChangeContent = input.onDidChangeContent; - isDirty(): boolean { return input.isDirty(); } - backup(): Promise { return input.backup(); } - save(): Promise { return input.save(); } - revert(options?: IRevertOptions): Promise { return input.revert(options); } + readonly resource = that._workingCopyResource; + get name() { return that._name; } + readonly capabilities = that.isUntitled() ? WorkingCopyCapabilities.Untitled : WorkingCopyCapabilities.None; + readonly onDidChangeDirty = that.onDidChangeDirty; + readonly onDidChangeContent = that.onDidChangeContent; + isDirty(): boolean { return that.isDirty(); } + backup(): Promise { return that.backup(); } + save(): Promise { return that.save(); } + revert(options?: IRevertOptions): Promise { return that.revert(options); } }; this._register(this._workingCopyService.registerWorkingCopy(workingCopyAdapter)); } + get lastResolvedFileStat() { + return this._lastResolvedFileStat; + } + + get notebook() { + return this._notebook; + } + + setDirty(newState: boolean) { + if (this._dirty !== newState) { + this._dirty = newState; + this._onDidChangeDirty.fire(); + } + } + async backup(): Promise> { if (this._notebook.supportBackup) { const tokenSource = new CancellationTokenSource(); @@ -115,13 +125,13 @@ export class NotebookEditorModel extends EditorModel implements INotebookEditorM const newStats = await this._resolveStats(this.resource); this._lastResolvedFileStat = newStats; - this._notebook.setDirty(false); + this.setDirty(false); this._onDidChangeDirty.fire(); } async load(options?: INotebookLoadOptions): Promise { if (options?.forceReadFromDisk) { - return this._loadFromProvider(true, undefined, undefined); + return this._loadFromProvider(true, undefined); } if (this.isResolved()) { @@ -134,11 +144,11 @@ export class NotebookEditorModel extends EditorModel implements INotebookEditorM return this; // Make sure meanwhile someone else did not succeed in loading } - return this._loadFromProvider(false, options?.editorId, backup?.meta?.backupId); + return this._loadFromProvider(false, backup?.meta?.backupId); } - private async _loadFromProvider(forceReloadFromDisk: boolean, editorId: string | undefined, backupId: string | undefined) { - this._notebook = await this._notebookService.resolveNotebook(this.viewType!, this.resource, forceReloadFromDisk, editorId, backupId); + private async _loadFromProvider(forceReloadFromDisk: boolean, backupId: string | undefined) { + this._notebook = await this._notebookService.resolveNotebook(this.viewType!, this.resource, forceReloadFromDisk, backupId); const newStats = await this._resolveStats(this.resource); this._lastResolvedFileStat = newStats; @@ -149,19 +159,19 @@ export class NotebookEditorModel extends EditorModel implements INotebookEditorM if (e.kind !== NotebookCellsChangeType.Initialize) { this._onDidChangeContent.fire(); } - })); - this._register(this._notebook.onDidChangeDirty(() => { - this._onDidChangeDirty.fire(); + if (!e.transient) { + this.setDirty(true); + } })); if (forceReloadFromDisk) { - this._notebook.setDirty(false); + this.setDirty(false); } if (backupId) { await this._backupFileService.discardBackup(this._workingCopyResource); - this._notebook.setDirty(true); + this.setDirty(true); } return this; @@ -172,7 +182,7 @@ export class NotebookEditorModel extends EditorModel implements INotebookEditorM } isDirty() { - return this._notebook?.isDirty; + return this._dirty; } isUntitled() { @@ -224,7 +234,7 @@ export class NotebookEditorModel extends EditorModel implements INotebookEditorM await this._notebookService.save(this.notebook.viewType, this.notebook.uri, tokenSource.token); const newStats = await this._resolveStats(this.resource); this._lastResolvedFileStat = newStats; - this._notebook.setDirty(false); + this.setDirty(false); return true; } @@ -244,7 +254,7 @@ export class NotebookEditorModel extends EditorModel implements INotebookEditorM await this._notebookService.saveAs(this.notebook.viewType, this.notebook.uri, targetResource, tokenSource.token); const newStats = await this._resolveStats(this.resource); this._lastResolvedFileStat = newStats; - this._notebook.setDirty(false); + this.setDirty(false); return true; } diff --git a/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverService.ts b/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverService.ts index 2206d05e61e..d10ed8444c7 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverService.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverService.ts @@ -16,7 +16,7 @@ export const INotebookEditorModelResolverService = createDecorator>; + resolve(resource: URI, viewType?: string): Promise>; } @@ -33,7 +33,7 @@ export class NotebookModelReferenceCollection extends ReferenceCollection { const resource = URI.parse(key); - let [viewType, editorId] = args as [string | undefined, string | undefined]; + let [viewType] = args as [string | undefined]; if (!viewType) { viewType = this._notebookService.getContributedNotebookProviders(resource)[0]?.id; } @@ -42,7 +42,7 @@ export class NotebookModelReferenceCollection extends ReferenceCollection> { - const reference = this._data.acquire(resource.toString(), viewType, editorId); + async resolve(resource: URI, viewType?: string): Promise> { + const reference = this._data.acquire(resource.toString(), viewType); const model = await reference.object; - NotebookModelResolverService._autoReferenceDirtyModel(model, () => this._data.acquire(resource.toString(), viewType, editorId)); + NotebookModelResolverService._autoReferenceDirtyModel(model, () => this._data.acquire(resource.toString(), viewType)); return { object: model, dispose() { reference.dispose(); } @@ -81,8 +81,8 @@ export class NotebookModelResolverService implements INotebookEditorModelResolve private static _autoReferenceDirtyModel(model: INotebookEditorModel, ref: () => IDisposable) { const references = new DisposableStore(); - const listener = model.notebook.onDidChangeDirty(() => { - if (model.notebook.isDirty) { + const listener = model.onDidChangeDirty(() => { + if (model.isDirty()) { references.add(ref()); } else { references.clear(); diff --git a/src/vs/workbench/contrib/notebook/common/notebookService.ts b/src/vs/workbench/contrib/notebook/common/notebookService.ts index 99a754b4e55..50de255b4e6 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookService.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookService.ts @@ -10,7 +10,7 @@ import { NotebookExtensionDescription } from 'vs/workbench/api/common/extHost.pr import { Event } from 'vs/base/common/event'; import { INotebookTextModel, INotebookRendererInfo, - IEditor, ICellEditOperation, NotebookCellOutputsSplice, INotebookKernelProvider, INotebookKernelInfo2, TransientMetadata + IEditor, ICellEditOperation, NotebookCellOutputsSplice, INotebookKernelProvider, INotebookKernelInfo2, TransientMetadata, NotebookDataDto, TransientOptions } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; import { CancellationToken } from 'vs/base/common/cancellation'; @@ -24,11 +24,10 @@ export const INotebookService = createDecorator('notebookServi export interface IMainNotebookController { supportBackup: boolean; options: { transientOutputs: boolean; transientMetadata: TransientMetadata; }; - createNotebook(textModel: NotebookTextModel, editorId?: string, backupId?: string): Promise; + resolveNotebookDocument(viewType: string, uri: URI, backupId?: string): Promise<{ data: NotebookDataDto, transientOptions: TransientOptions }>; reloadNotebook(mainthreadTextModel: NotebookTextModel): Promise; resolveNotebookEditor(viewType: string, uri: URI, editorId: string): Promise; onDidReceiveMessage(editorId: string, rendererType: string | undefined, message: any): void; - removeNotebookDocument(uri: URI): Promise; save(uri: URI, token: CancellationToken): Promise; saveAs(uri: URI, target: URI, token: CancellationToken): Promise; backup(uri: URI, token: CancellationToken): Promise; @@ -55,7 +54,7 @@ export interface INotebookService { getContributedNotebookOutputRenderers(id: string): NotebookOutputRendererInfo | undefined; getRendererInfo(id: string): INotebookRendererInfo | undefined; - resolveNotebook(viewType: string, uri: URI, forceReload: boolean, editorId?: string, backupId?: string): Promise; + resolveNotebook(viewType: string, uri: URI, forceReload: boolean, backupId?: string): Promise; getNotebookTextModel(uri: URI): NotebookTextModel | undefined; getNotebookTextModels(): Iterable; getContributedNotebookProviders(resource: URI): readonly NotebookProviderInfo[]; @@ -72,6 +71,7 @@ export interface INotebookService { getToCopy(): { items: NotebookCellTextModel[], isCopy: boolean; } | undefined; // editor events + resolveNotebookEditor(viewType: string, uri: URI, editorId: string): Promise; addNotebookEditor(editor: IEditor): void; removeNotebookEditor(editor: IEditor): void; getNotebookEditor(editorId: string): IEditor | undefined; diff --git a/src/vs/workbench/contrib/notebook/test/notebookViewModel.test.ts b/src/vs/workbench/contrib/notebook/test/notebookViewModel.test.ts index 92eae8a9b4a..a8d48a6870b 100644 --- a/src/vs/workbench/contrib/notebook/test/notebookViewModel.test.ts +++ b/src/vs/workbench/contrib/notebook/test/notebookViewModel.test.ts @@ -6,7 +6,7 @@ import * as assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; -import { CellKind, NotebookCellMetadata, diff, ICellRange } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellKind, NotebookCellMetadata, diff, ICellRange, notebookDocumentMetadataDefaults } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { withTestNotebook, TestCell, NotebookEditorTestModel, setupInstantiationService } from 'vs/workbench/contrib/notebook/test/testNotebookEditor'; import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; @@ -25,7 +25,7 @@ suite('NotebookViewModel', () => { const modeService = instantiationService.get(IModeService); test('ctor', function () { - const notebook = new NotebookTextModel('notebook', false, URI.parse('test'), undoRedoService, textModelService, modeService); + const notebook = new NotebookTextModel('notebook', false, URI.parse('test'), [], [], notebookDocumentMetadataDefaults, { transientMetadata: {}, transientOutputs: false }, undoRedoService, textModelService, modeService); const model = new NotebookEditorTestModel(notebook); const eventDispatcher = new NotebookEventDispatcher(); const viewModel = new NotebookViewModel('notebook', model.notebook, eventDispatcher, null, instantiationService, blukEditService, undoRedoService); diff --git a/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts b/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts index 31eab292f31..d2386aab84c 100644 --- a/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts +++ b/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts @@ -18,7 +18,7 @@ import { NotebookEventDispatcher } from 'vs/workbench/contrib/notebook/browser/v import { CellViewModel, IModelDecorationsChangeAccessor, NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; -import { CellKind, CellUri, INotebookEditorModel, IProcessedOutput, NotebookCellMetadata, IInsetRenderOutput, ICellRange, INotebookKernelInfo2 } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellKind, CellUri, INotebookEditorModel, IProcessedOutput, NotebookCellMetadata, IInsetRenderOutput, ICellRange, INotebookKernelInfo2, notebookDocumentMetadataDefaults } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { Webview } from 'vs/workbench/contrib/webview/browser/webview'; import { ICompositeCodeEditor, IEditor } from 'vs/editor/common/editorCommon'; import { NotImplementedError } from 'vs/base/common/errors'; @@ -397,8 +397,7 @@ export function withTestNotebook(instantiationService: TestInstantiationService, const viewType = 'notebook'; const editor = new TestNotebookEditor(); - const notebook = new NotebookTextModel(viewType, false, URI.parse('test'), undoRedoService, textModelService, modeService); - notebook.initialize(cells.map(cell => { + const notebook = new NotebookTextModel(viewType, false, URI.parse('test'), cells.map(cell => { return { source: cell[0], language: cell[1], @@ -406,7 +405,7 @@ export function withTestNotebook(instantiationService: TestInstantiationService, outputs: cell[3], metadata: cell[4] }; - })); + }), [], notebookDocumentMetadataDefaults, { transientMetadata: {}, transientOutputs: false }, undoRedoService, textModelService, modeService); const model = new NotebookEditorTestModel(notebook); const eventDispatcher = new NotebookEventDispatcher(); const viewModel = new NotebookViewModel(viewType, model.notebook, eventDispatcher, null, instantiationService, blukEditService, undoRedoService); diff --git a/src/vs/workbench/contrib/performance/electron-browser/startupProfiler.ts b/src/vs/workbench/contrib/performance/electron-browser/startupProfiler.ts index 2e0f791450a..3e8c73d7e28 100644 --- a/src/vs/workbench/contrib/performance/electron-browser/startupProfiler.ts +++ b/src/vs/workbench/contrib/performance/electron-browser/startupProfiler.ts @@ -55,7 +55,7 @@ export class StartupProfiler implements IWorkbenchContribution { const removeArgs: string[] = ['--prof-startup']; const markerFile = readFile(profileFilenamePrefix).then(value => removeArgs.push(...value.toString().split('|'))) .then(() => rimraf(profileFilenamePrefix)) // (1) delete the file to tell the main process to stop profiling - .then(() => new Promise(resolve => { // (2) wait for main that recreates the fail to signal profiling has stopped + .then(() => new Promise(resolve => { // (2) wait for main that recreates the fail to signal profiling has stopped const check = () => { exists(profileFilenamePrefix).then(exists => { if (exists) { diff --git a/src/vs/workbench/contrib/remote/browser/remote.ts b/src/vs/workbench/contrib/remote/browser/remote.ts index a59510f2aa3..af5e7bdee76 100644 --- a/src/vs/workbench/contrib/remote/browser/remote.ts +++ b/src/vs/workbench/contrib/remote/browser/remote.ts @@ -274,7 +274,7 @@ class HelpItemValue { constructor(private commandService: ICommandService, public extensionDescription: IExtensionDescription, public remoteAuthority: string[] | undefined, private urlOrCommand?: string) { } get url(): Promise { - return new Promise(async (resolve) => { + return new Promise(async (resolve) => { if (this._url === undefined) { if (this.urlOrCommand) { let url = URI.parse(this.urlOrCommand); diff --git a/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts b/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts index 3ee75fbc9c8..8dafe78b342 100644 --- a/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts +++ b/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts @@ -1301,7 +1301,7 @@ export class DirtyDiffWorkbenchController extends Disposable implements ext.IWor private setViewState(state: IViewState): void { this.viewState = state; - this.stylesheet.innerHTML = ` + this.stylesheet.textContent = ` .monaco-editor .dirty-diff-modified,.monaco-editor .dirty-diff-added{border-left-width:${state.width}px;} .monaco-editor .dirty-diff-modified, .monaco-editor .dirty-diff-added, .monaco-editor .dirty-diff-deleted { opacity: ${state.visibility === 'always' ? 1 : 0}; diff --git a/src/vs/workbench/contrib/search/browser/searchView.ts b/src/vs/workbench/contrib/search/browser/searchView.ts index 96521c068ed..7e2f15d238f 100644 --- a/src/vs/workbench/contrib/search/browser/searchView.ts +++ b/src/vs/workbench/contrib/search/browser/searchView.ts @@ -596,7 +596,7 @@ export class SearchView extends ViewPane { this.progressService.withProgress({ location: this.getProgressLocation(), delay: 100, total: occurrences }, p => { progressReporter = p; - return new Promise(resolve => progressComplete = resolve); + return new Promise(resolve => progressComplete = resolve); }); const confirmation: IConfirmation = { @@ -1398,7 +1398,7 @@ export class SearchView extends ViewPane { private doSearch(query: ITextQuery, excludePatternText: string, includePatternText: string, triggeredOnType: boolean): Thenable { let progressComplete: () => void; this.progressService.withProgress({ location: this.getProgressLocation(), delay: triggeredOnType ? 300 : 0 }, _progress => { - return new Promise(resolve => progressComplete = resolve); + return new Promise(resolve => progressComplete = resolve); }); this.searchWidget.searchInput.clearMessage(); diff --git a/src/vs/workbench/contrib/search/common/searchModel.ts b/src/vs/workbench/contrib/search/common/searchModel.ts index 983355a2396..8e8dd4acf61 100644 --- a/src/vs/workbench/contrib/search/common/searchModel.ts +++ b/src/vs/workbench/contrib/search/common/searchModel.ts @@ -737,7 +737,7 @@ export class SearchResult extends Disposable { set query(query: ITextQuery | null) { // When updating the query we could change the roots, so keep a reference to them to clean up when we trigger `disposePastResults` const oldFolderMatches = this.folderMatches(); - new Promise(resolve => this.disposePastResults = resolve) + new Promise(resolve => this.disposePastResults = resolve) .then(() => oldFolderMatches.forEach(match => match.clear())) .then(() => oldFolderMatches.forEach(match => match.dispose())) .then(() => this._isDirty = false); diff --git a/src/vs/workbench/contrib/search/test/common/cacheState.test.ts b/src/vs/workbench/contrib/search/test/common/cacheState.test.ts index 6baa982999a..242b2b17d1e 100644 --- a/src/vs/workbench/contrib/search/test/common/cacheState.test.ts +++ b/src/vs/workbench/contrib/search/test/common/cacheState.test.ts @@ -207,7 +207,7 @@ suite('FileQueryCacheState', () => { } public awaitDisposal(n: number) { - return new Promise(resolve => { + return new Promise(resolve => { if (n === Object.keys(this.disposing).length) { resolve(); } else { diff --git a/src/vs/workbench/contrib/snippets/browser/insertSnippet.ts b/src/vs/workbench/contrib/snippets/browser/insertSnippet.ts index 5b5c430ac30..fd8440e4056 100644 --- a/src/vs/workbench/contrib/snippets/browser/insertSnippet.ts +++ b/src/vs/workbench/contrib/snippets/browser/insertSnippet.ts @@ -91,7 +91,7 @@ class InsertSnippetAction extends EditorAction { const clipboardService = accessor.get(IClipboardService); const quickInputService = accessor.get(IQuickInputService); - const snippet = await new Promise(async (resolve, reject) => { + const snippet = await new Promise(async (resolve, reject) => { const { lineNumber, column } = editor.getPosition(); let { snippet, name, langId } = Args.fromUser(arg); diff --git a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts index 883e2ffcb85..7b7ba3dde69 100644 --- a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts +++ b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts @@ -2390,7 +2390,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer } picker.dispose(); if (!selection) { - resolve(); + resolve(undefined); } resolve(selection); })); diff --git a/src/vs/workbench/contrib/tasks/common/problemCollectors.ts b/src/vs/workbench/contrib/tasks/common/problemCollectors.ts index 70e9d1b1fb5..3a6610a6ee8 100644 --- a/src/vs/workbench/contrib/tasks/common/problemCollectors.ts +++ b/src/vs/workbench/contrib/tasks/common/problemCollectors.ts @@ -115,7 +115,7 @@ export abstract class AbstractProblemCollector implements IDisposable { } } - protected abstract async processLineInternal(line: string): Promise; + protected abstract processLineInternal(line: string): Promise; public dispose() { this.modelListeners.dispose(); diff --git a/src/vs/workbench/contrib/terminal/browser/links/terminalValidatedLocalLinkProvider.ts b/src/vs/workbench/contrib/terminal/browser/links/terminalValidatedLocalLinkProvider.ts index f53a11b5ab3..5c0e8243fd4 100644 --- a/src/vs/workbench/contrib/terminal/browser/links/terminalValidatedLocalLinkProvider.ts +++ b/src/vs/workbench/contrib/terminal/browser/links/terminalValidatedLocalLinkProvider.ts @@ -20,14 +20,14 @@ const pathPrefix = '(\\.\\.?|\\~)'; const pathSeparatorClause = '\\/'; // '":; are allowed in paths but they are often separators so ignore them // Also disallow \\ to prevent a catastropic backtracking case #24798 -const excludedPathCharactersClause = '[^\\0\\s!$`&*()\\[\\]+\'":;\\\\]'; +const excludedPathCharactersClause = '[^\\0\\s!$`&*()\\[\\]\'":;\\\\]'; /** A regex that matches paths in the form /foo, ~/foo, ./foo, ../foo, foo/bar */ export const unixLocalLinkClause = '((' + pathPrefix + '|(' + excludedPathCharactersClause + ')+)?(' + pathSeparatorClause + '(' + excludedPathCharactersClause + ')+)+)'; export const winDrivePrefix = '(?:\\\\\\\\\\?\\\\)?[a-zA-Z]:'; const winPathPrefix = '(' + winDrivePrefix + '|\\.\\.?|\\~)'; const winPathSeparatorClause = '(\\\\|\\/)'; -const winExcludedPathCharactersClause = '[^\\0<>\\?\\|\\/\\s!$`&*()\\[\\]+\'":;]'; +const winExcludedPathCharactersClause = '[^\\0<>\\?\\|\\/\\s!$`&*()\\[\\]\'":;]'; /** A regex that matches paths in the form \\?\c:\foo c:\foo, ~\foo, .\foo, ..\foo, foo\bar */ export const winLocalLinkClause = '((' + winPathPrefix + '|(' + winExcludedPathCharactersClause + ')+)?(' + winPathSeparatorClause + '(' + winExcludedPathCharactersClause + ')+)+)'; diff --git a/src/vs/workbench/contrib/terminal/browser/links/terminalWordLinkProvider.ts b/src/vs/workbench/contrib/terminal/browser/links/terminalWordLinkProvider.ts index c5a4d9cf383..a2cb37f78e4 100644 --- a/src/vs/workbench/contrib/terminal/browser/links/terminalWordLinkProvider.ts +++ b/src/vs/workbench/contrib/terminal/browser/links/terminalWordLinkProvider.ts @@ -53,7 +53,7 @@ export class TerminalWordLinkProvider extends TerminalBaseLinkProvider { // Add a link if this is a separator if (width !== 0 && wordSeparators.indexOf(chars) >= 0) { if (startX !== -1) { - result.push(new TerminalLink(this._xterm, { start: { x: startX + 1, y }, end: { x, y } }, text, this._xterm.buffer.active.viewportY, activateCallback, this._tooltipCallback, false, localize('searchWorkspace', 'Search workspace'), this._configurationService)); + result.push(this._createTerminalLink(startX, x, y, text, activateCallback)); text = ''; startX = -1; } @@ -70,12 +70,30 @@ export class TerminalWordLinkProvider extends TerminalBaseLinkProvider { // Add the final link if there is one if (startX !== -1) { - result.push(new TerminalLink(this._xterm, { start: { x: startX + 1, y }, end: { x: line.length, y } }, text, this._xterm.buffer.active.viewportY, activateCallback, this._tooltipCallback, false, localize('searchWorkspace', 'Search workspace'), this._configurationService)); + result.push(this._createTerminalLink(startX, line.length, y, text, activateCallback)); } return result; } + private _createTerminalLink(startX: number, endX: number, y: number, text: string, activateCallback: XtermLinkMatcherHandler): TerminalLink { + // Remove trailing colon if there is one so the link is more useful + if (text.length > 0 && text.charAt(text.length - 1) === ':') { + text = text.slice(0, -1); + endX--; + } + return this._instantiationService.createInstance(TerminalLink, + this._xterm, + { start: { x: startX + 1, y }, end: { x: endX, y } }, + text, + this._xterm.buffer.active.viewportY, + activateCallback, + this._tooltipCallback, + false, + localize('searchWorkspace', 'Search workspace') + ); + } + private async _activate(link: string) { const results = await this._searchService.fileSearch( this._fileQueryBuilder.file(this._workspaceContextService.getWorkspace().folders, { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts index 866cab0806d..363c3a00542 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts @@ -652,7 +652,7 @@ export function registerTerminalActions() { const codeEditorService = accessor.get(ICodeEditorService); const instance = terminalService.getActiveOrCreateInstance(); - let editor = codeEditorService.getFocusedCodeEditor(); + let editor = codeEditorService.getActiveCodeEditor(); if (!editor || !editor.hasModel()) { return; } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalConfigHelper.ts b/src/vs/workbench/contrib/terminal/browser/terminalConfigHelper.ts index e39451c4b69..6408853ea11 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalConfigHelper.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalConfigHelper.ts @@ -8,7 +8,7 @@ import * as platform from 'vs/base/common/platform'; import { EDITOR_FONT_DEFAULTS, IEditorOptions } from 'vs/editor/common/config/editorOptions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; -import { ITerminalConfiguration, ITerminalFont, IS_WORKSPACE_SHELL_ALLOWED_STORAGE_KEY, TERMINAL_CONFIG_SECTION, DEFAULT_LETTER_SPACING, DEFAULT_LINE_HEIGHT, MINIMUM_LETTER_SPACING, LinuxDistro, IShellLaunchConfig } from 'vs/workbench/contrib/terminal/common/terminal'; +import { ITerminalConfiguration, ITerminalFont, IS_WORKSPACE_SHELL_ALLOWED_STORAGE_KEY, TERMINAL_CONFIG_SECTION, DEFAULT_LETTER_SPACING, DEFAULT_LINE_HEIGHT, MINIMUM_LETTER_SPACING, LinuxDistro, IShellLaunchConfig, MINIMUM_FONT_WEIGHT, MAXIMUM_FONT_WEIGHT, DEFAULT_FONT_WEIGHT, DEFAULT_BOLD_FONT_WEIGHT, FontWeight } from 'vs/workbench/contrib/terminal/common/terminal'; import Severity from 'vs/base/common/severity'; import { INotificationService, NeverShowAgainScope } from 'vs/platform/notification/common/notification'; import { IBrowserTerminalConfigHelper } from 'vs/workbench/contrib/terminal/browser/terminal'; @@ -67,7 +67,11 @@ export class TerminalConfigHelper implements IBrowserTerminalConfigHelper { } private _updateConfig(): void { - this.config = this._configurationService.getValue(TERMINAL_CONFIG_SECTION); + const configValues = this._configurationService.getValue(TERMINAL_CONFIG_SECTION); + configValues.fontWeight = this._normalizeFontWeight(configValues.fontWeight, DEFAULT_FONT_WEIGHT); + configValues.fontWeightBold = this._normalizeFontWeight(configValues.fontWeightBold, DEFAULT_BOLD_FONT_WEIGHT); + + this.config = configValues; } public configFontIsMonospace(): boolean { @@ -157,7 +161,7 @@ export class TerminalConfigHelper implements IBrowserTerminalConfigHelper { const editorConfig = this._configurationService.getValue('editor'); let fontFamily = this.config.fontFamily || editorConfig.fontFamily || EDITOR_FONT_DEFAULTS.fontFamily; - let fontSize = this._toInteger(this.config.fontSize, MINIMUM_FONT_SIZE, MAXIMUM_FONT_SIZE, EDITOR_FONT_DEFAULTS.fontSize); + let fontSize = this._clampInt(this.config.fontSize, MINIMUM_FONT_SIZE, MAXIMUM_FONT_SIZE, EDITOR_FONT_DEFAULTS.fontSize); // Work around bad font on Fedora/Ubuntu if (!this.config.fontFamily) { @@ -168,7 +172,7 @@ export class TerminalConfigHelper implements IBrowserTerminalConfigHelper { fontFamily = '\'Ubuntu Mono\', monospace'; // Ubuntu mono is somehow smaller, so set fontSize a bit larger to get the same perceived size. - fontSize = this._toInteger(fontSize + 2, MINIMUM_FONT_SIZE, MAXIMUM_FONT_SIZE, EDITOR_FONT_DEFAULTS.fontSize); + fontSize = this._clampInt(fontSize + 2, MINIMUM_FONT_SIZE, MAXIMUM_FONT_SIZE, EDITOR_FONT_DEFAULTS.fontSize); } } @@ -271,7 +275,7 @@ export class TerminalConfigHelper implements IBrowserTerminalConfigHelper { return !!isWorkspaceShellAllowed; } - private _toInteger(source: any, minimum: number, maximum: number, fallback: number): number { + private _clampInt(source: any, minimum: number, maximum: number, fallback: T): number | T { let r = parseInt(source, 10); if (isNaN(r)) { return fallback; @@ -340,4 +344,11 @@ export class TerminalConfigHelper implements IBrowserTerminalConfigHelper { const extensions = await this._extensionManagementService.getInstalled(ExtensionType.User); return extensions.some(e => e.identifier.id === id); } + + private _normalizeFontWeight(input: any, defaultWeight: FontWeight): FontWeight { + if (input === 'normal' || input === 'bold') { + return input; + } + return this._clampInt(input, MINIMUM_FONT_WEIGHT, MAXIMUM_FONT_WEIGHT, defaultWeight); + } } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalProcessExtHostProxy.ts b/src/vs/workbench/contrib/terminal/browser/terminalProcessExtHostProxy.ts index 7d7623c1a0d..84f18d3c998 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalProcessExtHostProxy.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalProcessExtHostProxy.ts @@ -43,9 +43,9 @@ export class TerminalProcessExtHostProxy extends Disposable implements ITerminal private readonly _onRequestLatency = this._register(new Emitter()); public readonly onRequestLatency: Event = this._onRequestLatency.event; - private _pendingInitialCwdRequests: ((value?: string | Thenable) => void)[] = []; - private _pendingCwdRequests: ((value?: string | Thenable) => void)[] = []; - private _pendingLatencyRequests: ((value?: number | Thenable) => void)[] = []; + private _pendingInitialCwdRequests: ((value: string | PromiseLike) => void)[] = []; + private _pendingCwdRequests: ((value: string | PromiseLike) => void)[] = []; + private _pendingLatencyRequests: ((value: number | PromiseLike) => void)[] = []; constructor( public terminalId: number, diff --git a/src/vs/workbench/contrib/terminal/common/terminal.ts b/src/vs/workbench/contrib/terminal/common/terminal.ts index 9a41a37b16b..1af8a10e218 100644 --- a/src/vs/workbench/contrib/terminal/common/terminal.ts +++ b/src/vs/workbench/contrib/terminal/common/terminal.ts @@ -70,7 +70,13 @@ export const DEFAULT_LETTER_SPACING = 0; export const MINIMUM_LETTER_SPACING = -5; export const DEFAULT_LINE_HEIGHT = 1; -export type FontWeight = 'normal' | 'bold' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900'; +export const MINIMUM_FONT_WEIGHT = 1; +export const MAXIMUM_FONT_WEIGHT = 1000; +export const DEFAULT_FONT_WEIGHT = 'normal'; +export const DEFAULT_BOLD_FONT_WEIGHT = 'bold'; +export const SUGGESTIONS_FONT_WEIGHT = ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900']; + +export type FontWeight = 'normal' | 'bold' | number; export interface ITerminalConfiguration { shell: { diff --git a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts index f38353bcb8d..985fde8246b 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts @@ -6,7 +6,7 @@ import { IConfigurationNode } from 'vs/platform/configuration/common/configurationRegistry'; import { localize } from 'vs/nls'; import { EDITOR_FONT_DEFAULTS } from 'vs/editor/common/config/editorOptions'; -import { DEFAULT_LETTER_SPACING, DEFAULT_LINE_HEIGHT, TerminalCursorStyle, DEFAULT_COMMANDS_TO_SKIP_SHELL } from 'vs/workbench/contrib/terminal/common/terminal'; +import { DEFAULT_LETTER_SPACING, DEFAULT_LINE_HEIGHT, TerminalCursorStyle, DEFAULT_COMMANDS_TO_SKIP_SHELL, SUGGESTIONS_FONT_WEIGHT, MINIMUM_FONT_WEIGHT, MAXIMUM_FONT_WEIGHT } from 'vs/workbench/contrib/terminal/common/terminal'; import { isMacintosh, isWindows, Platform } from 'vs/base/common/platform'; export const terminalConfiguration: IConfigurationNode = { @@ -136,15 +136,41 @@ export const terminalConfiguration: IConfigurationNode = { default: 1 }, 'terminal.integrated.fontWeight': { - type: 'string', - enum: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'], - description: localize('terminal.integrated.fontWeight', "The font weight to use within the terminal for non-bold text."), + 'anyOf': [ + { + type: 'number', + minimum: MINIMUM_FONT_WEIGHT, + maximum: MAXIMUM_FONT_WEIGHT, + errorMessage: localize('terminal.integrated.fontWeightError', "Only \"normal\" and \"bold\" keywords or numbers between 1 and 1000 are allowed.") + }, + { + type: 'string', + pattern: '^(normal|bold|1000|[1-9][0-9]{0,2})$' + }, + { + enum: SUGGESTIONS_FONT_WEIGHT, + } + ], + description: localize('terminal.integrated.fontWeight', "The font weight to use within the terminal for non-bold text. Accepts \"normal\" and \"bold\" keywords or numbers between 1 and 1000."), default: 'normal' }, 'terminal.integrated.fontWeightBold': { - type: 'string', - enum: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'], - description: localize('terminal.integrated.fontWeightBold', "The font weight to use within the terminal for bold text."), + 'anyOf': [ + { + type: 'number', + minimum: MINIMUM_FONT_WEIGHT, + maximum: MAXIMUM_FONT_WEIGHT, + errorMessage: localize('terminal.integrated.fontWeightError', "Only \"normal\" and \"bold\" keywords or numbers between 1 and 1000 are allowed.") + }, + { + type: 'string', + pattern: '^(normal|bold|1000|[1-9][0-9]{0,2})$' + }, + { + enum: SUGGESTIONS_FONT_WEIGHT, + } + ], + description: localize('terminal.integrated.fontWeightBold', "The font weight to use within the terminal for bold text. Accepts \"normal\" and \"bold\" keywords or numbers between 1 and 1000."), default: 'bold' }, 'terminal.integrated.cursorBlinking': { diff --git a/src/vs/workbench/contrib/terminal/test/browser/links/terminalValidatedLocalLinkProvider.test.ts b/src/vs/workbench/contrib/terminal/test/browser/links/terminalValidatedLocalLinkProvider.test.ts index 23f81babf84..d6950b086ef 100644 --- a/src/vs/workbench/contrib/terminal/test/browser/links/terminalValidatedLocalLinkProvider.test.ts +++ b/src/vs/workbench/contrib/terminal/test/browser/links/terminalValidatedLocalLinkProvider.test.ts @@ -19,7 +19,9 @@ const unixLinks = [ './foo', '../foo', '/foo/bar', - 'foo/bar' + '/foo/bar+more', + 'foo/bar', + 'foo/bar+more', ]; const windowsLinks = [ @@ -33,10 +35,12 @@ const windowsLinks = [ '~/foo', 'c:/foo/bar', 'c:\\foo\\bar', + 'c:\\foo\\bar+more', 'c:\\foo/bar\\baz', 'foo/bar', 'foo/bar', - 'foo\\bar' + 'foo\\bar', + 'foo\\bar+more', ]; interface LinkFormatInfo { diff --git a/src/vs/workbench/contrib/terminal/test/browser/links/terminalWordLinkProvider.test.ts b/src/vs/workbench/contrib/terminal/test/browser/links/terminalWordLinkProvider.test.ts index 4927a1cbc37..9a9bbab0b06 100644 --- a/src/vs/workbench/contrib/terminal/test/browser/links/terminalWordLinkProvider.test.ts +++ b/src/vs/workbench/contrib/terminal/test/browser/links/terminalWordLinkProvider.test.ts @@ -79,4 +79,13 @@ suite('Workbench - TerminalWordLinkProvider', () => { { range: [[5, 1], [7, 1]], text: 'bar' } ]); }); + + test('should remove trailing colon in the link results', async () => { + await configurationService.setUserConfiguration('terminal', { integrated: { wordSeparators: ' ' } }); + await assertLink('foo:5:6: bar:0:32:', [ + { range: [[1, 1], [7, 1]], text: 'foo:5:6' }, + { range: [[10, 1], [17, 1]], text: 'bar:0:32' } + ]); + }); + }); diff --git a/src/vs/workbench/contrib/themes/browser/themes.contribution.ts b/src/vs/workbench/contrib/themes/browser/themes.contribution.ts index dca30b971db..aec062d251c 100644 --- a/src/vs/workbench/contrib/themes/browser/themes.contribution.ts +++ b/src/vs/workbench/contrib/themes/browser/themes.contribution.ts @@ -147,7 +147,7 @@ abstract class AbstractIconThemeAction extends Action { }, applyTheme ? 0 : 200); }; - return new Promise((s, _) => { + return new Promise((s, _) => { let isCompleted = false; const autoFocusIndex = firstIndex(picks, p => isItem(p) && p.id === currentTheme.id); diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts index d8ed519d348..cf6a3bf0793 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts @@ -949,7 +949,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo }); } run(accessor: ServicesAccessor): any { - return new Promise((c, e) => { + return new Promise((c, e) => { const quickInputService = accessor.get(IQuickInputService); const commandService = accessor.get(ICommandService); const disposables = new DisposableStore(); diff --git a/src/vs/workbench/contrib/webview/browser/baseWebviewElement.ts b/src/vs/workbench/contrib/webview/browser/baseWebviewElement.ts index 4423555621a..04ebf3bfae0 100644 --- a/src/vs/workbench/contrib/webview/browser/baseWebviewElement.ts +++ b/src/vs/workbench/contrib/webview/browser/baseWebviewElement.ts @@ -171,8 +171,10 @@ export abstract class BaseWebview extends Disposable { if (this.element) { this.element.remove(); } - this._element = undefined; + + this._onDidDispose.fire(); + super.dispose(); } @@ -203,6 +205,9 @@ export abstract class BaseWebview extends Disposable { private readonly _onDidBlur = this._register(new Emitter()); public readonly onDidBlur = this._onDidBlur.event; + private readonly _onDidDispose = this._register(new Emitter()); + public readonly onDidDispose = this._onDidDispose.event; + public postMessage(data: any): void { this._send('message', data); } diff --git a/src/vs/workbench/contrib/webview/browser/dynamicWebviewEditorOverlay.ts b/src/vs/workbench/contrib/webview/browser/dynamicWebviewEditorOverlay.ts index 7d2046676ff..72c446f7004 100644 --- a/src/vs/workbench/contrib/webview/browser/dynamicWebviewEditorOverlay.ts +++ b/src/vs/workbench/contrib/webview/browser/dynamicWebviewEditorOverlay.ts @@ -59,12 +59,12 @@ export class DynamicWebviewEditorOverlay extends Disposable implements WebviewOv return !!this._webview.value?.isFocused; } - private readonly _onDispose = this._register(new Emitter()); - public onDispose = this._onDispose.event; + private readonly _onDidDispose = this._register(new Emitter()); + public onDidDispose = this._onDidDispose.event; dispose() { this.container.remove(); - this._onDispose.fire(); + this._onDidDispose.fire(); super.dispose(); } diff --git a/src/vs/workbench/contrib/webview/browser/webview.contribution.ts b/src/vs/workbench/contrib/webview/browser/webview.contribution.ts index 25d4ea314fd..9144fa8eea0 100644 --- a/src/vs/workbench/contrib/webview/browser/webview.contribution.ts +++ b/src/vs/workbench/contrib/webview/browser/webview.contribution.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { MultiCommand, RedoCommand, SelectAllCommand, ServicesAccessor, UndoCommand } from 'vs/editor/browser/editorExtensions'; +import { MultiCommand, RedoCommand, SelectAllCommand, UndoCommand } from 'vs/editor/browser/editorExtensions'; import { CopyAction, CutAction, PasteAction } from 'vs/editor/contrib/clipboard/clipboard'; import { localize } from 'vs/nls'; import { registerAction2 } from 'vs/platform/actions/common/actions'; @@ -12,11 +12,12 @@ import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { Registry } from 'vs/platform/registry/common/platform'; import { EditorDescriptor, Extensions as EditorExtensions, IEditorRegistry } from 'vs/workbench/browser/editor'; import { Extensions as EditorInputExtensions, IEditorInputFactoryRegistry } from 'vs/workbench/common/editor'; -import { Webview, WebviewOverlay } from 'vs/workbench/contrib/webview/browser/webview'; +import { IWebviewService, Webview } from 'vs/workbench/contrib/webview/browser/webview'; import { WebviewEditorInputFactory } from 'vs/workbench/contrib/webview/browser/webviewEditorInputFactory'; -import { getActiveWebviewEditor, HideWebViewEditorFindCommand, ReloadWebviewAction, ShowWebViewEditorFindWidgetAction, WebViewEditorFindNextCommand, WebViewEditorFindPreviousCommand } from '../browser/webviewCommands'; +import { HideWebViewEditorFindCommand, ReloadWebviewAction, ShowWebViewEditorFindWidgetAction, WebViewEditorFindNextCommand, WebViewEditorFindPreviousCommand } from '../browser/webviewCommands'; import { WebviewEditor } from './webviewEditor'; import { WebviewInput } from './webviewEditorInput'; +import { WebviewService } from './webviewService'; import { IWebviewWorkbenchService, WebviewEditorService } from './webviewWorkbenchService'; (Registry.as(EditorExtensions.Editors)).registerEditor(EditorDescriptor.create( @@ -29,6 +30,7 @@ Registry.as(EditorInputExtensions.EditorInputFactor WebviewEditorInputFactory.ID, WebviewEditorInputFactory); +registerSingleton(IWebviewService, WebviewService, true); registerSingleton(IWebviewWorkbenchService, WebviewEditorService, true); registerAction2(ShowWebViewEditorFindWidgetAction); @@ -38,32 +40,13 @@ registerAction2(WebViewEditorFindPreviousCommand); registerAction2(ReloadWebviewAction); -function getInnerActiveWebview(accessor: ServicesAccessor): Webview | undefined { - const webview = getActiveWebviewEditor(accessor); - if (!webview) { - return undefined; - } - - // Make sure we are really focused on the webview - if (!['WEBVIEW', 'IFRAME'].includes(document.activeElement?.tagName ?? '')) { - return undefined; - } - - if ('getInnerWebview' in (webview as WebviewOverlay)) { - const innerWebview = (webview as WebviewOverlay).getInnerWebview(); - return innerWebview; - } - - return webview; -} - - const PRIORITY = 100; function overrideCommandForWebview(command: MultiCommand | undefined, f: (webview: Webview) => void) { command?.addImplementation(PRIORITY, accessor => { - const webview = getInnerActiveWebview(accessor); - if (webview && webview.isFocused) { + const webviewService = accessor.get(IWebviewService); + const webview = webviewService.activeWebview; + if (webview?.isFocused) { f(webview); return true; } diff --git a/src/vs/workbench/contrib/webview/browser/webview.ts b/src/vs/workbench/contrib/webview/browser/webview.ts index 4df30f8be7b..61c085a241c 100644 --- a/src/vs/workbench/contrib/webview/browser/webview.ts +++ b/src/vs/workbench/contrib/webview/browser/webview.ts @@ -36,6 +36,8 @@ export interface WebviewIcons { export interface IWebviewService { readonly _serviceBrand: undefined; + readonly activeWebview: Webview | undefined; + createWebviewElement( id: string, options: WebviewOptions, @@ -100,6 +102,8 @@ export interface Webview extends IDisposable { readonly onDidFocus: Event; readonly onDidBlur: Event; + readonly onDidDispose: Event; + readonly onDidClickLink: Event; readonly onDidScroll: Event<{ scrollYPercentage: number }>; readonly onDidWheel: Event; @@ -142,8 +146,6 @@ export interface WebviewOverlay extends Webview { readonly container: HTMLElement; options: WebviewOptions; - readonly onDispose: Event; - claim(owner: any): void; release(owner: any): void; diff --git a/src/vs/workbench/contrib/webview/browser/webviewService.ts b/src/vs/workbench/contrib/webview/browser/webviewService.ts index 688b513948d..a26e64ab978 100644 --- a/src/vs/workbench/contrib/webview/browser/webviewService.ts +++ b/src/vs/workbench/contrib/webview/browser/webviewService.ts @@ -3,34 +3,39 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IWebviewService, WebviewContentOptions, WebviewOverlay, WebviewElement, WebviewIcons, WebviewOptions, WebviewExtensionDescription } from 'vs/workbench/contrib/webview/browser/webview'; -import { IFrameWebview } from 'vs/workbench/contrib/webview/browser/webviewElement'; import { WebviewThemeDataProvider } from 'vs/workbench/contrib/webview/browser/themeing'; +import { IWebviewService, Webview, WebviewContentOptions, WebviewElement, WebviewExtensionDescription, WebviewIcons, WebviewOptions, WebviewOverlay } from 'vs/workbench/contrib/webview/browser/webview'; +import { IFrameWebview } from 'vs/workbench/contrib/webview/browser/webviewElement'; import { DynamicWebviewEditorOverlay } from './dynamicWebviewEditorOverlay'; import { WebviewIconManager } from './webviewIconManager'; export class WebviewService implements IWebviewService { declare readonly _serviceBrand: undefined; - private readonly _webviewThemeDataProvider: WebviewThemeDataProvider; + protected readonly _webviewThemeDataProvider: WebviewThemeDataProvider; + private readonly _iconManager: WebviewIconManager; constructor( - @IInstantiationService private readonly _instantiationService: IInstantiationService, + @IInstantiationService protected readonly _instantiationService: IInstantiationService, ) { this._webviewThemeDataProvider = this._instantiationService.createInstance(WebviewThemeDataProvider); this._iconManager = this._instantiationService.createInstance(WebviewIconManager); } + private _activeWebview?: Webview; + public get activeWebview() { return this._activeWebview; } + createWebviewElement( id: string, options: WebviewOptions, contentOptions: WebviewContentOptions, extension: WebviewExtensionDescription | undefined, ): WebviewElement { - return this._instantiationService.createInstance(IFrameWebview, id, options, contentOptions, extension, this._webviewThemeDataProvider); + const webview = this._instantiationService.createInstance(IFrameWebview, id, options, contentOptions, extension, this._webviewThemeDataProvider); + this.addWebviewListeners(webview); + return webview; } createWebviewOverlay( @@ -39,12 +44,27 @@ export class WebviewService implements IWebviewService { contentOptions: WebviewContentOptions, extension: WebviewExtensionDescription | undefined, ): WebviewOverlay { - return this._instantiationService.createInstance(DynamicWebviewEditorOverlay, id, options, contentOptions, extension); + const webview = this._instantiationService.createInstance(DynamicWebviewEditorOverlay, id, options, contentOptions, extension); + this.addWebviewListeners(webview); + return webview; } setIcons(id: string, iconPath: WebviewIcons | undefined): void { this._iconManager.setIcons(id, iconPath); } -} -registerSingleton(IWebviewService, WebviewService, true); + protected addWebviewListeners(webview: Webview) { + webview.onDidFocus(() => { + this._activeWebview = webview; + }); + + const onBlur = () => { + if (this._activeWebview === webview) { + this._activeWebview = undefined; + } + }; + + webview.onDidBlur(onBlur); + webview.onDidDispose(onBlur); + } +} diff --git a/src/vs/workbench/contrib/webview/electron-browser/webviewService.ts b/src/vs/workbench/contrib/webview/electron-browser/webviewService.ts index 44f3d38b9e9..480f2212768 100644 --- a/src/vs/workbench/contrib/webview/electron-browser/webviewService.ts +++ b/src/vs/workbench/contrib/webview/electron-browser/webviewService.ts @@ -6,24 +6,19 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { DynamicWebviewEditorOverlay } from 'vs/workbench/contrib/webview/browser/dynamicWebviewEditorOverlay'; -import { WebviewThemeDataProvider } from 'vs/workbench/contrib/webview/browser/themeing'; -import { IWebviewService, WebviewContentOptions, WebviewElement, WebviewExtensionDescription, WebviewIcons, WebviewOptions, WebviewOverlay } from 'vs/workbench/contrib/webview/browser/webview'; -import { WebviewIconManager } from 'vs/workbench/contrib/webview/browser/webviewIconManager'; +import { WebviewContentOptions, WebviewElement, WebviewExtensionDescription, WebviewOptions, WebviewOverlay } from 'vs/workbench/contrib/webview/browser/webview'; +import { WebviewService } from 'vs/workbench/contrib/webview/browser/webviewService'; import { ElectronIframeWebview } from 'vs/workbench/contrib/webview/electron-browser/iframeWebviewElement'; import { ElectronWebviewBasedWebview } from 'vs/workbench/contrib/webview/electron-browser/webviewElement'; -export class ElectronWebviewService implements IWebviewService { +export class ElectronWebviewService extends WebviewService { declare readonly _serviceBrand: undefined; - private readonly _webviewThemeDataProvider: WebviewThemeDataProvider; - private readonly _iconManager: WebviewIconManager; - constructor( - @IInstantiationService private readonly _instantiationService: IInstantiationService, + @IInstantiationService instantiationService: IInstantiationService, @IConfigurationService private readonly _configService: IConfigurationService, ) { - this._webviewThemeDataProvider = this._instantiationService.createInstance(WebviewThemeDataProvider); - this._iconManager = this._instantiationService.createInstance(WebviewIconManager); + super(instantiationService); } createWebviewElement( @@ -33,7 +28,9 @@ export class ElectronWebviewService implements IWebviewService { extension: WebviewExtensionDescription | undefined, ): WebviewElement { const useIframes = this._configService.getValue('webview.experimental.useIframes'); - return this._instantiationService.createInstance(useIframes ? ElectronIframeWebview : ElectronWebviewBasedWebview, id, options, contentOptions, extension, this._webviewThemeDataProvider); + const webview = this._instantiationService.createInstance(useIframes ? ElectronIframeWebview : ElectronWebviewBasedWebview, id, options, contentOptions, extension, this._webviewThemeDataProvider); + this.addWebviewListeners(webview); + return webview; } createWebviewOverlay( @@ -42,10 +39,8 @@ export class ElectronWebviewService implements IWebviewService { contentOptions: WebviewContentOptions, extension: WebviewExtensionDescription | undefined, ): WebviewOverlay { - return this._instantiationService.createInstance(DynamicWebviewEditorOverlay, id, options, contentOptions, extension); - } - - setIcons(id: string, iconPath: WebviewIcons | undefined): void { - this._iconManager.setIcons(id, iconPath); + const webview = this._instantiationService.createInstance(DynamicWebviewEditorOverlay, id, options, contentOptions, extension); + this.addWebviewListeners(webview); + return webview; } } diff --git a/src/vs/workbench/contrib/webviewView/browser/webviewViewPane.ts b/src/vs/workbench/contrib/webviewView/browser/webviewViewPane.ts index ad5ba7e2796..8e2849173f7 100644 --- a/src/vs/workbench/contrib/webviewView/browser/webviewViewPane.ts +++ b/src/vs/workbench/contrib/webviewView/browser/webviewViewPane.ts @@ -28,7 +28,10 @@ import { IExtensionService } from 'vs/workbench/services/extensions/common/exten declare const ResizeObserver: any; -const webviewStateKey = 'webviewState'; +const storageKeys = { + webviewState: 'webviewState', + title: 'title' +} as const; export class WebviewViewPane extends ViewPane { @@ -38,6 +41,9 @@ export class WebviewViewPane extends ViewPane { private _container?: HTMLElement; private _resizeObserver?: any; + private readonly defaultTitle: string; + private setTitle: string | undefined; + private readonly memento: Memento; private readonly viewState: MementoObject; @@ -60,10 +66,16 @@ export class WebviewViewPane extends ViewPane { @IViewsService private readonly viewService: IViewsService, ) { super({ ...options, titleMenuId: MenuId.ViewTitle }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); + this.defaultTitle = this.title; this.memento = new Memento(`webviewView.${this.id}`, storageService); this.viewState = this.memento.getMemento(StorageScope.WORKSPACE); + const storedTitle = this.viewState[storageKeys.title]; + if (typeof storedTitle === 'string') { + this.updateTitle(storedTitle); + } + this._register(this.onDidChangeBodyVisibility(() => this.updateTreeVisibility())); this.updateTreeVisibility(); } @@ -108,8 +120,9 @@ export class WebviewViewPane extends ViewPane { public saveState() { if (this._webview) { - this.viewState[webviewStateKey] = this._webview.state; + this.viewState[storageKeys.webviewState] = this._webview.state; } + this.viewState[storageKeys.title] = this.setTitle; this.memento.saveMemento(); super.saveState(); @@ -142,7 +155,7 @@ export class WebviewViewPane extends ViewPane { const webviewId = `webviewView-${this.id.replace(/[^a-z0-9]/gi, '-')}`.toLowerCase(); const webview = this.webviewService.createWebviewOverlay(webviewId, {}, {}, undefined); - webview.state = this.viewState['webviewState']; + webview.state = this.viewState[storageKeys.webviewState]; this._webview = webview; this._register(toDisposable(() => { @@ -150,7 +163,7 @@ export class WebviewViewPane extends ViewPane { })); this._register(webview.onDidUpdateState(() => { - this.viewState[webviewStateKey] = webview.state; + this.viewState[storageKeys.webviewState] = webview.state; })); const source = this._register(new CancellationTokenSource()); @@ -164,8 +177,8 @@ export class WebviewViewPane extends ViewPane { onDidChangeVisibility: this.onDidChangeBodyVisibility, onDispose: this.onDispose, - get title() { return self.title; }, - set title(value: string) { self.updateTitle(value); }, + get title(): string | undefined { return self.setTitle; }, + set title(value: string | undefined) { self.updateTitle(value); }, get description(): string | undefined { return self.titleDescription; }, set description(value: string | undefined) { self.updateTitleDescription(value); }, @@ -180,6 +193,11 @@ export class WebviewViewPane extends ViewPane { } } + protected updateTitle(value: string | undefined) { + this.setTitle = value; + super.updateTitle(typeof value === 'string' ? value : this.defaultTitle); + } + private async withProgress(task: () => Promise): Promise { return this.progressService.withProgress({ location: this.id, delay: 500 }, task); } diff --git a/src/vs/workbench/electron-sandbox/sandbox.simpleservices.ts b/src/vs/workbench/electron-sandbox/sandbox.simpleservices.ts index 1159fe856e7..181d779e376 100644 --- a/src/vs/workbench/electron-sandbox/sandbox.simpleservices.ts +++ b/src/vs/workbench/electron-sandbox/sandbox.simpleservices.ts @@ -599,6 +599,8 @@ registerSingleton(IKeymapService, SimpleKeymapService); class SimpleWebviewService implements IWebviewService { declare readonly _serviceBrand: undefined; + readonly activeWebview = undefined; + createWebviewElement(id: string, options: WebviewOptions, contentOptions: WebviewContentOptions, extension: WebviewExtensionDescription | undefined): WebviewElement { throw new Error('Method not implemented.'); } createWebviewOverlay(id: string, options: WebviewOptions, contentOptions: WebviewContentOptions, extension: WebviewExtensionDescription | undefined): WebviewOverlay { throw new Error('Method not implemented.'); } setIcons(id: string, value: WebviewIcons | undefined): void { } diff --git a/src/vs/workbench/services/authentication/browser/authenticationService.ts b/src/vs/workbench/services/authentication/browser/authenticationService.ts index 82a27ec5e0c..db85f09a426 100644 --- a/src/vs/workbench/services/authentication/browser/authenticationService.ts +++ b/src/vs/workbench/services/authentication/browser/authenticationService.ts @@ -329,7 +329,7 @@ export class AuthenticationService extends Disposable implements IAuthentication // Activate has already been called for the authentication provider, but it cannot block on registering itself // since this is sync and returns a disposable. So, wait for registration event to fire that indicates the // provider is now in the map. - await new Promise((resolve, _) => { + await new Promise((resolve, _) => { this.onDidRegisterAuthenticationProvider(e => { if (e.id === providerId) { provider = this._authenticationProviders.get(providerId); @@ -444,7 +444,12 @@ export class AuthenticationService extends Disposable implements IAuthentication const didRegister: Promise = new Promise((resolve, _) => { this.onDidRegisterAuthenticationProvider(e => { if (e.id === providerId) { - resolve(this._authenticationProviders.get(providerId)); + provider = this._authenticationProviders.get(providerId); + if (provider) { + resolve(provider); + } else { + throw new Error(`No authentication provider '${providerId}' is currently registered.`); + } } }); }); diff --git a/src/vs/workbench/services/configuration/test/electron-browser/configurationService.test.ts b/src/vs/workbench/services/configuration/test/electron-browser/configurationService.test.ts index e04efb9e339..56879ce1a36 100644 --- a/src/vs/workbench/services/configuration/test/electron-browser/configurationService.test.ts +++ b/src/vs/workbench/services/configuration/test/electron-browser/configurationService.test.ts @@ -1191,14 +1191,14 @@ suite('WorkspaceConfigurationService - Folder', () => { test('change event when there are global tasks', () => { fs.writeFileSync(globalTasksFile, '{ "version": "1.0.0", "tasks": [{ "taskName": "myTask" }'); - return new Promise((c) => testObject.onDidChangeConfiguration(() => c())); + return new Promise((c) => testObject.onDidChangeConfiguration(() => c())); }); test('creating workspace settings', async () => { fs.writeFileSync(globalSettingsFile, '{ "configurationService.folder.testSetting": "userValue" }'); await testObject.reloadConfiguration(); const workspaceSettingsResource = URI.file(path.join(workspaceDir, '.vscode', 'settings.json')); - await new Promise(async (c) => { + await new Promise(async (c) => { const disposable = testObject.onDidChangeConfiguration(e => { assert.ok(e.affectsConfiguration('configurationService.folder.testSetting')); assert.equal(testObject.getValue('configurationService.folder.testSetting'), 'workspaceValue'); @@ -1217,7 +1217,7 @@ suite('WorkspaceConfigurationService - Folder', () => { const workspaceSettingsResource = URI.file(path.join(workspaceDir, '.vscode', 'settings.json')); await fileService.writeFile(workspaceSettingsResource, VSBuffer.fromString('{ "configurationService.folder.testSetting": "workspaceValue" }')); await testObject.reloadConfiguration(); - await new Promise(async (c) => { + await new Promise(async (c) => { const disposable = testObject.onDidChangeConfiguration(e => { assert.ok(e.affectsConfiguration('configurationService.folder.testSetting')); assert.equal(testObject.getValue('configurationService.folder.testSetting'), 'userValue'); @@ -1822,7 +1822,7 @@ suite('WorkspaceConfigurationService-Multiroot', () => { await workspaceService.removeFolders([uri]); fs.writeFileSync(path.join(uri.fsPath, '.vscode', 'settings.json'), '{ "configurationService.workspace.testResourceSetting": "workspaceFolderValue" }'); - return new Promise((c, e) => { + return new Promise((c, e) => { testObject.onDidChangeConfiguration(() => { try { assert.equal(testObject.getValue('configurationService.workspace.testResourceSetting', { resource: uri }), 'workspaceFolderValue'); @@ -1948,7 +1948,7 @@ suite('WorkspaceConfigurationService - Remote Folder', () => { fs.writeFileSync(remoteSettingsFile, '{ "configurationService.remote.machineSetting": "remoteValue" }'); registerRemoteFileSystemProvider(); await initialize(); - const promise = new Promise((c, e) => { + const promise = new Promise((c, e) => { testObject.onDidChangeConfiguration(event => { try { assert.equal(event.source, ConfigurationTarget.USER); @@ -1968,7 +1968,7 @@ suite('WorkspaceConfigurationService - Remote Folder', () => { fs.writeFileSync(remoteSettingsFile, '{ "configurationService.remote.machineSetting": "remoteValue" }'); registerRemoteFileSystemProviderOnActivation(); await initialize(); - const promise = new Promise((c, e) => { + const promise = new Promise((c, e) => { testObject.onDidChangeConfiguration(event => { try { assert.equal(event.source, ConfigurationTarget.USER); @@ -1989,7 +1989,7 @@ suite('WorkspaceConfigurationService - Remote Folder', () => { resolveRemoteEnvironment(); await initialize(); assert.equal(testObject.getValue('configurationService.remote.machineSetting'), 'isSet'); - const promise = new Promise((c, e) => { + const promise = new Promise((c, e) => { testObject.onDidChangeConfiguration(event => { try { assert.equal(event.source, ConfigurationTarget.USER); diff --git a/src/vs/workbench/services/decorations/test/browser/decorationsService.test.ts b/src/vs/workbench/services/decorations/test/browser/decorationsService.test.ts index 2f1298ea85b..b129b381291 100644 --- a/src/vs/workbench/services/decorations/test/browser/decorationsService.test.ts +++ b/src/vs/workbench/services/decorations/test/browser/decorationsService.test.ts @@ -94,7 +94,7 @@ suite('DecorationsService', function () { // un-register -> ensure good event let didSeeEvent = false; - let p = new Promise(resolve => { + let p = new Promise(resolve => { service.onDidChangeDecorations(e => { assert.equal(e.affectsResource(uri), true); assert.deepEqual(service.getDecoration(uri, false), undefined); @@ -275,7 +275,7 @@ suite('DecorationsService', function () { data = service.getDecoration(uri2, true)!; assert.ok(data.tooltip); // emphazied items... - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { let l = service.onDidChangeDecorations(e => { l.dispose(); try { diff --git a/src/vs/workbench/services/editor/common/editorOpenWith.ts b/src/vs/workbench/services/editor/common/editorOpenWith.ts index a824ac2e408..807464f51f8 100644 --- a/src/vs/workbench/services/editor/common/editorOpenWith.ts +++ b/src/vs/workbench/services/editor/common/editorOpenWith.ts @@ -62,13 +62,13 @@ export async function openEditorWith( // Prompt const resourceExt = extname(resource); - const items: (IQuickPickItem & { handler: IOpenEditorOverrideHandler })[] = allEditorOverrides.map((override) => { + const items: (IQuickPickItem & { handler: IOpenEditorOverrideHandler })[] = allEditorOverrides.map(([handler, entry]) => { return { - handler: override[0], - id: override[1].id, - label: override[1].label, - description: override[1].active ? nls.localize('promptOpenWith.currentlyActive', 'Currently Active') : undefined, - detail: override[1].detail, + handler: handler, + id: entry.id, + label: entry.label, + description: entry.active ? nls.localize('promptOpenWith.currentlyActive', 'Currently Active') : undefined, + detail: entry.detail, buttons: resourceExt ? [{ iconClass: 'codicon-settings-gear', tooltip: nls.localize('promptOpenWith.setDefaultTooltip', "Set as default editor for '{0}' files", resourceExt) diff --git a/src/vs/workbench/services/progress/browser/progressService.ts b/src/vs/workbench/services/progress/browser/progressService.ts index d1a79df4f13..89f4eafc97e 100644 --- a/src/vs/workbench/services/progress/browser/progressService.ts +++ b/src/vs/workbench/services/progress/browser/progressService.ts @@ -224,9 +224,9 @@ export class ProgressService extends Disposable implements IProgressService { // Create a promise that we can resolve as needed // when the outside calls dispose on us let promiseResolve: () => void; - const promise = new Promise(resolve => promiseResolve = resolve); + const promise = new Promise(resolve => promiseResolve = resolve); - this.withWindowProgress({ + this.withWindowProgress({ location: ProgressLocation.Window, title: options.title ? parseLinkedText(options.title).toString() : undefined, // convert markdown links => string command: 'notifications.showList' diff --git a/src/vs/workbench/services/textMate/browser/abstractTextMateService.ts b/src/vs/workbench/services/textMate/browser/abstractTextMateService.ts index 6567c19b739..d718890fc2b 100644 --- a/src/vs/workbench/services/textMate/browser/abstractTextMateService.ts +++ b/src/vs/workbench/services/textMate/browser/abstractTextMateService.ts @@ -312,7 +312,7 @@ export abstract class AbstractTextMateService extends Disposable implements ITex grammarFactory.setTheme(theme, tokenColorMap); let colorMap = AbstractTextMateService._toColorMap(tokenColorMap); let cssRules = generateTokensCSSForColorMap(colorMap); - this._styleElement.innerHTML = cssRules; + this._styleElement.textContent = cssRules; TokenizationRegistry.setColorMap(colorMap); } diff --git a/src/vs/workbench/services/textmodelResolver/test/browser/textModelResolverService.test.ts b/src/vs/workbench/services/textmodelResolver/test/browser/textModelResolverService.test.ts index f8b2464c808..a8abe3cce15 100644 --- a/src/vs/workbench/services/textmodelResolver/test/browser/textModelResolverService.test.ts +++ b/src/vs/workbench/services/textmodelResolver/test/browser/textModelResolverService.test.ts @@ -58,7 +58,7 @@ suite('Workbench - TextModelResolverService', () => { assert.ok(model); assert.equal(snapshotToString(((model as ResourceEditorModel).createSnapshot()!)), 'Hello Test'); let disposed = false; - let disposedPromise = new Promise(resolve => { + let disposedPromise = new Promise(resolve => { Event.once(model.onDispose)(() => { disposed = true; resolve(); @@ -201,7 +201,7 @@ suite('Workbench - TextModelResolverService', () => { modelRef1.dispose(); assert(!textModel.isDisposed(), 'the text model should still not be disposed'); - let p1 = new Promise(resolve => textModel.onWillDispose(resolve)); + let p1 = new Promise(resolve => textModel.onWillDispose(resolve)); modelRef2.dispose(); await p1; diff --git a/src/vs/workbench/services/themes/browser/workbenchThemeService.ts b/src/vs/workbench/services/themes/browser/workbenchThemeService.ts index 39c18b0c7d3..cbce827b1db 100644 --- a/src/vs/workbench/services/themes/browser/workbenchThemeService.ts +++ b/src/vs/workbench/services/themes/browser/workbenchThemeService.ts @@ -685,10 +685,10 @@ function _applyRules(styleSheetContent: string, rulesClassName: string) { const elStyle = document.createElement('style'); elStyle.type = 'text/css'; elStyle.className = rulesClassName; - elStyle.innerHTML = styleSheetContent; + elStyle.textContent = styleSheetContent; document.head.appendChild(elStyle); } else { - (themeStyles[0]).innerHTML = styleSheetContent; + (themeStyles[0]).textContent = styleSheetContent; } } diff --git a/src/vs/workbench/services/workspaces/browser/abstractWorkspaceEditingService.ts b/src/vs/workbench/services/workspaces/browser/abstractWorkspaceEditingService.ts index 067eae83070..479d04fbade 100644 --- a/src/vs/workbench/services/workspaces/browser/abstractWorkspaceEditingService.ts +++ b/src/vs/workbench/services/workspaces/browser/abstractWorkspaceEditingService.ts @@ -307,7 +307,7 @@ export abstract class AbstractWorkspaceEditingService implements IWorkspaceEditi ); } - abstract async enterWorkspace(path: URI): Promise; + abstract enterWorkspace(path: URI): Promise; protected async doEnterWorkspace(path: URI): Promise { if (!!this.environmentService.extensionTestsLocationURI) { diff --git a/src/vs/workbench/test/browser/api/extHostApiCommands.test.ts b/src/vs/workbench/test/browser/api/extHostApiCommands.test.ts index 6a7252b386d..f5290dd2f74 100644 --- a/src/vs/workbench/test/browser/api/extHostApiCommands.test.ts +++ b/src/vs/workbench/test/browser/api/extHostApiCommands.test.ts @@ -1011,6 +1011,29 @@ suite('ExtHostLanguageFeatureCommands', function () { }); }); + test('What\'s the condition for DocumentLink target to be undefined? #106308', async function () { + disposables.push(extHost.registerDocumentLinkProvider(nullExtensionDescription, defaultSelector, { + provideDocumentLinks(): any { + return [new types.DocumentLink(new types.Range(0, 0, 0, 20), undefined)]; + }, + resolveDocumentLink(link) { + link.target = URI.parse('foo:bar'); + return link; + } + })); + + await rpcProtocol.sync(); + + const links1 = await commands.executeCommand('vscode.executeLinkProvider', model.uri); + assert.equal(links1.length, 1); + assert.equal(links1[0].target, undefined); + + const links2 = await commands.executeCommand('vscode.executeLinkProvider', model.uri, 1000); + assert.equal(links2.length, 1); + assert.equal(links2[0].target!.toString(), URI.parse('foo:bar').toString()); + + }); + test('Color provider', function () { diff --git a/src/vs/workbench/test/browser/api/extHostTextEditors.test.ts b/src/vs/workbench/test/browser/api/extHostBulkEdits.test.ts similarity index 80% rename from src/vs/workbench/test/browser/api/extHostTextEditors.test.ts rename to src/vs/workbench/test/browser/api/extHostBulkEdits.test.ts index a1778a00358..f6adea9dd43 100644 --- a/src/vs/workbench/test/browser/api/extHostTextEditors.test.ts +++ b/src/vs/workbench/test/browser/api/extHostBulkEdits.test.ts @@ -4,26 +4,26 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; import * as extHostTypes from 'vs/workbench/api/common/extHostTypes'; -import { MainContext, MainThreadTextEditorsShape, IWorkspaceEditDto, WorkspaceEditType } from 'vs/workbench/api/common/extHost.protocol'; +import { MainContext, IWorkspaceEditDto, WorkspaceEditType, MainThreadBulkEditsShape } from 'vs/workbench/api/common/extHost.protocol'; import { URI } from 'vs/base/common/uri'; import { mock } from 'vs/base/test/common/mock'; import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; import { SingleProxyRPCProtocol, TestRPCProtocol } from 'vs/workbench/test/browser/api/testRPCProtocol'; -import { ExtHostEditors } from 'vs/workbench/api/common/extHostTextEditors'; import { NullLogService } from 'vs/platform/log/common/log'; import { assertType } from 'vs/base/common/types'; +import { ExtHostBulkEdits } from 'vs/workbench/api/common/extHostBulkEdits'; -suite('ExtHostTextEditors.applyWorkspaceEdit', () => { +suite('ExtHostBulkEdits.applyWorkspaceEdit', () => { const resource = URI.parse('foo:bar'); - let editors: ExtHostEditors; + let bulkEdits: ExtHostBulkEdits; let workspaceResourceEdits: IWorkspaceEditDto; setup(() => { workspaceResourceEdits = null!; let rpcProtocol = new TestRPCProtocol(); - rpcProtocol.set(MainContext.MainThreadTextEditors, new class extends mock() { + rpcProtocol.set(MainContext.MainThreadBulkEdits, new class extends mock() { $tryApplyWorkspaceEdit(_workspaceResourceEdits: IWorkspaceEditDto): Promise { workspaceResourceEdits = _workspaceResourceEdits; return Promise.resolve(true); @@ -40,13 +40,13 @@ suite('ExtHostTextEditors.applyWorkspaceEdit', () => { EOL: '\n', }] }); - editors = new ExtHostEditors(rpcProtocol, documentsAndEditors, null!); + bulkEdits = new ExtHostBulkEdits(rpcProtocol, documentsAndEditors, null!); }); test('uses version id if document available', async () => { let edit = new extHostTypes.WorkspaceEdit(); edit.replace(resource, new extHostTypes.Range(0, 0, 0, 0), 'hello'); - await editors.applyWorkspaceEdit(edit); + await bulkEdits.applyWorkspaceEdit(edit); assert.equal(workspaceResourceEdits.edits.length, 1); const [first] = workspaceResourceEdits.edits; assertType(first._type === WorkspaceEditType.Text); @@ -56,7 +56,7 @@ suite('ExtHostTextEditors.applyWorkspaceEdit', () => { test('does not use version id if document is not available', async () => { let edit = new extHostTypes.WorkspaceEdit(); edit.replace(URI.parse('foo:bar2'), new extHostTypes.Range(0, 0, 0, 0), 'hello'); - await editors.applyWorkspaceEdit(edit); + await bulkEdits.applyWorkspaceEdit(edit); assert.equal(workspaceResourceEdits.edits.length, 1); const [first] = workspaceResourceEdits.edits; assertType(first._type === WorkspaceEditType.Text); diff --git a/src/vs/workbench/test/browser/api/extHostDocumentSaveParticipant.test.ts b/src/vs/workbench/test/browser/api/extHostDocumentSaveParticipant.test.ts index 855f92b34de..8c4732b5915 100644 --- a/src/vs/workbench/test/browser/api/extHostDocumentSaveParticipant.test.ts +++ b/src/vs/workbench/test/browser/api/extHostDocumentSaveParticipant.test.ts @@ -7,7 +7,7 @@ import { URI } from 'vs/base/common/uri'; import { ExtHostDocuments } from 'vs/workbench/api/common/extHostDocuments'; import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; import { TextDocumentSaveReason, TextEdit, Position, EndOfLine } from 'vs/workbench/api/common/extHostTypes'; -import { MainThreadTextEditorsShape, IWorkspaceEditDto, IWorkspaceTextEditDto } from 'vs/workbench/api/common/extHost.protocol'; +import { MainThreadTextEditorsShape, IWorkspaceEditDto, IWorkspaceTextEditDto, MainThreadBulkEditsShape } from 'vs/workbench/api/common/extHost.protocol'; import { ExtHostDocumentSaveParticipant } from 'vs/workbench/api/common/extHostDocumentSaveParticipant'; import { SingleProxyRPCProtocol } from './testRPCProtocol'; import { SaveReason } from 'vs/workbench/common/editor'; @@ -20,7 +20,7 @@ import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensio suite('ExtHostDocumentSaveParticipant', () => { let resource = URI.parse('foo:bar'); - let mainThreadEditors = new class extends mock() { }; + let mainThreadBulkEdits = new class extends mock() { }; let documents: ExtHostDocuments; let nullLogService = new NullLogService(); let nullExtensionDescription: IExtensionDescription = { @@ -51,12 +51,12 @@ suite('ExtHostDocumentSaveParticipant', () => { }); test('no listeners, no problem', () => { - const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, mainThreadEditors); + const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, mainThreadBulkEdits); return participant.$participateInSave(resource, SaveReason.EXPLICIT).then(() => assert.ok(true)); }); test('event delivery', () => { - const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, mainThreadEditors); + const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, mainThreadBulkEdits); let event: vscode.TextDocumentWillSaveEvent; let sub = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (e) { @@ -73,7 +73,7 @@ suite('ExtHostDocumentSaveParticipant', () => { }); test('event delivery, immutable', () => { - const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, mainThreadEditors); + const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, mainThreadBulkEdits); let event: vscode.TextDocumentWillSaveEvent; let sub = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (e) { @@ -89,7 +89,7 @@ suite('ExtHostDocumentSaveParticipant', () => { }); test('event delivery, bad listener', () => { - const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, mainThreadEditors); + const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, mainThreadBulkEdits); let sub = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (e) { throw new Error('💀'); @@ -104,7 +104,7 @@ suite('ExtHostDocumentSaveParticipant', () => { }); test('event delivery, bad listener doesn\'t prevent more events', () => { - const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, mainThreadEditors); + const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, mainThreadBulkEdits); let sub1 = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (e) { throw new Error('💀'); @@ -123,7 +123,7 @@ suite('ExtHostDocumentSaveParticipant', () => { }); test('event delivery, in subscriber order', () => { - const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, mainThreadEditors); + const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, mainThreadBulkEdits); let counter = 0; let sub1 = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (event) { @@ -141,7 +141,7 @@ suite('ExtHostDocumentSaveParticipant', () => { }); test('event delivery, ignore bad listeners', async () => { - const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, mainThreadEditors, { timeout: 5, errors: 1 }); + const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, mainThreadBulkEdits, { timeout: 5, errors: 1 }); let callCount = 0; let sub = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (event) { @@ -159,7 +159,7 @@ suite('ExtHostDocumentSaveParticipant', () => { }); test('event delivery, overall timeout', () => { - const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, mainThreadEditors, { timeout: 20, errors: 5 }); + const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, mainThreadBulkEdits, { timeout: 20, errors: 5 }); let callCount = 0; let sub1 = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (event) { @@ -187,7 +187,7 @@ suite('ExtHostDocumentSaveParticipant', () => { }); test('event delivery, waitUntil', () => { - const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, mainThreadEditors); + const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, mainThreadBulkEdits); let sub = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (event) { @@ -203,11 +203,11 @@ suite('ExtHostDocumentSaveParticipant', () => { }); test('event delivery, waitUntil must be called sync', () => { - const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, mainThreadEditors); + const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, mainThreadBulkEdits); let sub = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (event) { - event.waitUntil(new Promise((resolve, reject) => { + event.waitUntil(new Promise((resolve, reject) => { setTimeout(() => { try { assert.throws(() => event.waitUntil(timeout(10))); @@ -226,7 +226,7 @@ suite('ExtHostDocumentSaveParticipant', () => { }); test('event delivery, waitUntil will timeout', () => { - const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, mainThreadEditors, { timeout: 5, errors: 3 }); + const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, mainThreadBulkEdits, { timeout: 5, errors: 3 }); let sub = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (event) { event.waitUntil(timeout(15)); @@ -241,7 +241,7 @@ suite('ExtHostDocumentSaveParticipant', () => { }); test('event delivery, waitUntil failure handling', () => { - const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, mainThreadEditors); + const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, mainThreadBulkEdits); let sub1 = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (e) { e.waitUntil(Promise.reject(new Error('dddd'))); @@ -380,7 +380,7 @@ suite('ExtHostDocumentSaveParticipant', () => { error(message: string | Error, ...args: any[]): void { didLogSomething = true; } - }, documents, mainThreadEditors); + }, documents, mainThreadBulkEdits); let sub = participant.getOnWillSaveTextDocumentEvent(nullExtensionDescription)(function (e) { diff --git a/src/vs/workbench/test/browser/api/extHostNotebook.test.ts b/src/vs/workbench/test/browser/api/extHostNotebook.test.ts index 2f6156164f1..d1093e90efe 100644 --- a/src/vs/workbench/test/browser/api/extHostNotebook.test.ts +++ b/src/vs/workbench/test/browser/api/extHostNotebook.test.ts @@ -11,7 +11,8 @@ import { DisposableStore } from 'vs/base/common/lifecycle'; import { NullLogService } from 'vs/platform/log/common/log'; import { mock } from 'vs/base/test/common/mock'; import { IModelAddedData, MainContext, MainThreadCommandsShape, MainThreadNotebookShape } from 'vs/workbench/api/common/extHost.protocol'; -import { ExtHostNotebookDocument, ExtHostNotebookController } from 'vs/workbench/api/common/extHostNotebook'; +import { ExtHostNotebookController } from 'vs/workbench/api/common/extHostNotebook'; +import { ExtHostNotebookDocument } from 'vs/workbench/api/common/extHostNotebookDocument'; import { CellKind, CellUri, NotebookCellsChangeType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { URI } from 'vs/base/common/uri'; import { ExtHostDocuments } from 'vs/workbench/api/common/extHostDocuments'; @@ -134,7 +135,7 @@ suite('NotebookCell#Document', function () { test('cell document is vscode.TextDocument after changing it', async function () { - const p = new Promise((resolve, reject) => { + const p = new Promise((resolve, reject) => { extHostNotebooks.onDidChangeNotebookCells(e => { try { assert.strictEqual(e.changes.length, 1); diff --git a/src/vs/workbench/test/browser/api/extHostNotebookConcatDocument.test.ts b/src/vs/workbench/test/browser/api/extHostNotebookConcatDocument.test.ts index c9f18c332a9..71e9d8216c8 100644 --- a/src/vs/workbench/test/browser/api/extHostNotebookConcatDocument.test.ts +++ b/src/vs/workbench/test/browser/api/extHostNotebookConcatDocument.test.ts @@ -9,7 +9,8 @@ import { ExtHostDocuments } from 'vs/workbench/api/common/extHostDocuments'; import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; import { NullLogService } from 'vs/platform/log/common/log'; import { ExtHostNotebookConcatDocument } from 'vs/workbench/api/common/extHostNotebookConcatDocument'; -import { ExtHostNotebookDocument, ExtHostNotebookController } from 'vs/workbench/api/common/extHostNotebook'; +import { ExtHostNotebookController } from 'vs/workbench/api/common/extHostNotebook'; +import { ExtHostNotebookDocument } from 'vs/workbench/api/common/extHostNotebookDocument'; import { URI } from 'vs/base/common/uri'; import { CellKind, CellUri, NotebookCellsChangeType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { Position, Location, Range } from 'vs/workbench/api/common/extHostTypes'; diff --git a/src/vs/workbench/test/browser/api/extHostTextEditor.test.ts b/src/vs/workbench/test/browser/api/extHostTextEditor.test.ts index dc844a5273a..931313c55e9 100644 --- a/src/vs/workbench/test/browser/api/extHostTextEditor.test.ts +++ b/src/vs/workbench/test/browser/api/extHostTextEditor.test.ts @@ -84,7 +84,6 @@ suite('ExtHostTextEditorOptions', () => { $tryRevealRange: undefined!, $trySetSelections: undefined!, $tryApplyEdits: undefined!, - $tryApplyWorkspaceEdit: undefined!, $tryInsertSnippet: undefined!, $getDiffInformation: undefined! }; diff --git a/src/vs/workbench/test/browser/api/extHostTreeViews.test.ts b/src/vs/workbench/test/browser/api/extHostTreeViews.test.ts index 9ba5a4e9f1b..690b204329a 100644 --- a/src/vs/workbench/test/browser/api/extHostTreeViews.test.ts +++ b/src/vs/workbench/test/browser/api/extHostTreeViews.test.ts @@ -248,7 +248,7 @@ suite('ExtHostTreeView', function () { }); async function runWithEventMerging(action: (resolve: () => void) => void) { - await new Promise((resolve) => { + await new Promise((resolve) => { let subscription: IDisposable | undefined = undefined; subscription = target.onRefresh.event(() => { subscription!.dispose(); @@ -256,7 +256,7 @@ suite('ExtHostTreeView', function () { }); onDidChangeTreeNode.fire(getNode('b')); }); - await new Promise(action); + await new Promise(action); } test('refresh parent and child node trigger refresh only on parent - scenario 1', async () => { diff --git a/src/vs/workbench/test/browser/api/mainThreadDocumentContentProviders.test.ts b/src/vs/workbench/test/browser/api/mainThreadDocumentContentProviders.test.ts index 22fb7d3502f..f8f606f9829 100644 --- a/src/vs/workbench/test/browser/api/mainThreadDocumentContentProviders.test.ts +++ b/src/vs/workbench/test/browser/api/mainThreadDocumentContentProviders.test.ts @@ -35,7 +35,7 @@ suite('MainThreadDocumentContentProviders', function () { }, ); - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { let expectedEvents = 1; model.onDidChangeContent(e => { expectedEvents -= 1;