From cb4076e563719dc2b29761f8239cbcf4de585d49 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 18 Nov 2024 10:37:26 -0800 Subject: [PATCH 001/119] Decorations wip Part of #227095 --- .../browser/gpu/fullFileRenderStrategy.ts | 49 ++++++++++++++++++- .../browser/gpu/raster/glyphRasterizer.ts | 6 +++ src/vs/editor/browser/gpu/viewGpuContext.ts | 9 +++- 3 files changed, 61 insertions(+), 3 deletions(-) diff --git a/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts b/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts index a6df706d2b89e..a42148b222561 100644 --- a/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts +++ b/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts @@ -7,9 +7,12 @@ import { getActiveWindow } from '../../../base/browser/dom.js'; import { BugIndicatingError } from '../../../base/common/errors.js'; import { EditorOption } from '../../common/config/editorOptions.js'; import { CursorColumns } from '../../common/core/cursorColumns.js'; +import { Range } from '../../common/core/range.js'; +import { MetadataConsts } from '../../common/encodedTokenAttributes.js'; +import { ClassName } from '../../common/model/intervalTree.js'; import type { IViewLineTokens } from '../../common/tokens/lineTokens.js'; import { ViewEventHandler } from '../../common/viewEventHandler.js'; -import { ViewEventType, type ViewConfigurationChangedEvent, type ViewLinesChangedEvent, type ViewLinesDeletedEvent, type ViewLinesInsertedEvent, type ViewScrollChangedEvent, type ViewTokensChangedEvent, type ViewZonesChangedEvent } from '../../common/viewEvents.js'; +import { ViewEventType, type ViewConfigurationChangedEvent, type ViewDecorationsChangedEvent, type ViewLinesChangedEvent, type ViewLinesDeletedEvent, type ViewLinesInsertedEvent, type ViewScrollChangedEvent, type ViewTokensChangedEvent, type ViewZonesChangedEvent } from '../../common/viewEvents.js'; import type { ViewportData } from '../../common/viewLayout/viewLinesViewportData.js'; import type { ViewLineRenderingData } from '../../common/viewModel.js'; import type { ViewContext } from '../../common/viewModel/viewContext.js'; @@ -115,6 +118,13 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend return true; } + public override onDecorationsChanged(e: ViewDecorationsChangedEvent): boolean { + // TODO: Don't clear all lines + this._upToDateLines[0].clear(); + this._upToDateLines[1].clear(); + return true; + } + public override onTokensChanged(e: ViewTokensChangedEvent): boolean { // TODO: This currently fires for the entire viewport whenever scrolling stops // https://github.com/microsoft/vscode/issues/233942 @@ -274,6 +284,8 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend } } + const decorations = viewportData.getDecorationsInViewport(); + for (y = viewportData.startLineNumber; y <= viewportData.endLineNumber; y++) { // Only attempt to render lines that the GPU renderer can handle @@ -291,6 +303,14 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend dirtyLineStart = Math.min(dirtyLineStart, y); dirtyLineEnd = Math.max(dirtyLineEnd, y); + const inlineDecorations = decorations.filter(e => ( + e.range.startLineNumber <= y && e.range.endLineNumber >= y && + e.options.inlineClassName + )); + if (inlineDecorations.length > 0) { + console.log('decoration!', inlineDecorations); + } + lineData = viewportData.getViewLineRenderingData(y); content = lineData.content; xOffset = 0; @@ -313,6 +333,33 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend break; } chars = content.charAt(x); + + // TODO: We'd want to optimize pulling the decorations in order + // HACK: Temporary replace char to demonstrate inline decorations + const cellDecorations = decorations.filter(decoration => { + // TODO: Why does Range.containsPosition and Range.strictContainsPosition not work here? + if (y < decoration.range.startLineNumber || y > decoration.range.endLineNumber) { + return false; + } + if (y === decoration.range.startLineNumber && x < decoration.range.startColumn - 1) { + return false; + } + if (y === decoration.range.endLineNumber && x >= decoration.range.endColumn - 1) { + return false; + } + return true; + }); + for (const decoration of cellDecorations) { + switch (decoration.options.inlineClassName) { + case (ClassName.EditorDeprecatedInlineDecoration): { + // HACK: We probably shouldn't override tokenMetadata + tokenMetadata |= MetadataConsts.STRIKETHROUGH_MASK; + // chars = '-'; + break; + } + } + } + if (chars === ' ' || chars === '\t') { // Zero out glyph to ensure it doesn't get rendered cellIndex = ((y - 1) * this._viewGpuContext.maxGpuCols + x) * Constants.IndicesPerCell; diff --git a/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts b/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts index 26010b7e38690..07d343bb677c4 100644 --- a/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts +++ b/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts @@ -120,6 +120,12 @@ export class GlyphRasterizer extends Disposable implements IGlyphRasterizer { this._ctx.textBaseline = 'top'; this._ctx.fillText(chars, originX, originY); + // TODO: Don't draw beyond glyph - how to handle monospace, wide and proportional? + // TODO: Support strikethrough color + if (fontStyle & FontStyle.Strikethrough) { + this._ctx.fillRect(originX, originY + Math.round(devicePixelFontSize / 2), devicePixelFontSize, Math.max(Math.floor(getActiveWindow().devicePixelRatio), 1)); + } + const imageData = this._ctx.getImageData(0, 0, this._canvas.width, this._canvas.height); this._findGlyphBoundingBox(imageData, this._workGlyph.boundingBox); // const offset = { diff --git a/src/vs/editor/browser/gpu/viewGpuContext.ts b/src/vs/editor/browser/gpu/viewGpuContext.ts index 307636b37e1fe..310a6f97f5d1e 100644 --- a/src/vs/editor/browser/gpu/viewGpuContext.ts +++ b/src/vs/editor/browser/gpu/viewGpuContext.ts @@ -19,6 +19,7 @@ import { GPULifecycle } from './gpuDisposable.js'; import { ensureNonNullable, observeDevicePixelDimensions } from './gpuUtils.js'; import { RectangleRenderer } from './rectangleRenderer.js'; import type { ViewContext } from '../../common/viewModel/viewContext.js'; +import { ClassName } from '../../common/model/intervalTree.js'; const enum GpuRenderLimits { maxGpuLines = 3000, @@ -131,7 +132,8 @@ export class ViewGpuContext extends Disposable { data.containsRTL || data.maxColumn > GpuRenderLimits.maxGpuCols || data.continuesWithWrappedLine || - data.inlineDecorations.length > 0 || + // HACK: ... + data.inlineDecorations.length > 0 && data.inlineDecorations[0].inlineClassName !== ClassName.EditorDeprecatedInlineDecoration || lineNumber >= GpuRenderLimits.maxGpuLines ) { return false; @@ -155,7 +157,10 @@ export class ViewGpuContext extends Disposable { reasons.push('continuesWithWrappedLine'); } if (data.inlineDecorations.length > 0) { - reasons.push('inlineDecorations > 0'); + // HACK: ... + if (data.inlineDecorations[0].inlineClassName !== ClassName.EditorDeprecatedInlineDecoration) { + reasons.push('inlineDecorations > 0'); + } } if (lineNumber >= GpuRenderLimits.maxGpuLines) { reasons.push('lineNumber >= maxGpuLines'); From 8354641acf0adb64e6b881bbdc91345bd9dc05d5 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 19 Nov 2024 07:58:57 -0800 Subject: [PATCH 002/119] Prototype for extracting inline decoration styles --- src/vs/editor/browser/gpu/cssRuleExtractor.ts | 77 +++++++++++++++++++ .../browser/gpu/fullFileRenderStrategy.ts | 27 ++++++- 2 files changed, 101 insertions(+), 3 deletions(-) create mode 100644 src/vs/editor/browser/gpu/cssRuleExtractor.ts diff --git a/src/vs/editor/browser/gpu/cssRuleExtractor.ts b/src/vs/editor/browser/gpu/cssRuleExtractor.ts new file mode 100644 index 0000000000000..d8bd41ac385d7 --- /dev/null +++ b/src/vs/editor/browser/gpu/cssRuleExtractor.ts @@ -0,0 +1,77 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { $, getActiveDocument } from '../../../base/browser/dom.js'; +import { Disposable, toDisposable } from '../../../base/common/lifecycle.js'; +import type { ViewGpuContext } from './viewGpuContext.js'; + +export class CssRuleExtractor extends Disposable { + private _container: HTMLElement; + + private _ruleCache: Map = new Map(); + + constructor( + private readonly _viewGpuContext: ViewGpuContext, + ) { + super(); + + this._container = $('div.monaco-css-rule-extractor'); + this._container.style.visibility = 'hidden'; + const parentElement = this._viewGpuContext.canvas.domNode.parentElement; + if (!parentElement) { + throw new Error('No parent element found for the canvas'); + } + parentElement.appendChild(this._container); + this._register(toDisposable(() => this._container.remove())); + } + + getStyleRules(className: string): CSSStyleRule[] { + const existing = this._ruleCache.get(className); + if (existing) { + return existing; + } + const dummyElement = $(`span.${className}`); + this._container.appendChild(dummyElement); + const rules = this._getStyleRules(dummyElement, className); + this._ruleCache.set(className, rules); + return rules; + } + + private _getStyleRules(element: HTMLElement, className: string) { + const matchedRules = []; + + // Iterate through all stylesheets + const doc = getActiveDocument(); + for (const stylesheet of doc.styleSheets) { + try { + // Iterate through all CSS rules in the stylesheet + for (const rule of stylesheet.cssRules) { + if (rule instanceof CSSImportRule) { + // Recursively process the import rule + if (rule.styleSheet?.cssRules) { + for (const innerRule of rule.styleSheet.cssRules) { + if (innerRule instanceof CSSStyleRule) { + if (element.matches(innerRule.selectorText) && innerRule.selectorText.includes(className)) { + matchedRules.push(innerRule); + } + } + } + } + } else if (rule instanceof CSSStyleRule) { + // Check if the element matches the selector + if (element.matches(rule.selectorText) && rule.selectorText.includes(className)) { + matchedRules.push(rule); + } + } + } + } catch (e) { + // Some stylesheets may not be accessible due to CORS restrictions + console.warn('Could not access stylesheet:', stylesheet.href); + } + } + + return matchedRules; + } +} diff --git a/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts b/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts index a42148b222561..a57f6c9f085fe 100644 --- a/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts +++ b/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts @@ -3,11 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { getActiveWindow } from '../../../base/browser/dom.js'; +import { getActiveDocument, getActiveWindow } from '../../../base/browser/dom.js'; import { BugIndicatingError } from '../../../base/common/errors.js'; import { EditorOption } from '../../common/config/editorOptions.js'; import { CursorColumns } from '../../common/core/cursorColumns.js'; -import { Range } from '../../common/core/range.js'; import { MetadataConsts } from '../../common/encodedTokenAttributes.js'; import { ClassName } from '../../common/model/intervalTree.js'; import type { IViewLineTokens } from '../../common/tokens/lineTokens.js'; @@ -18,6 +17,7 @@ import type { ViewLineRenderingData } from '../../common/viewModel.js'; import type { ViewContext } from '../../common/viewModel/viewContext.js'; import type { ViewLineOptions } from '../viewParts/viewLines/viewLineOptions.js'; import type { ITextureAtlasPageGlyph } from './atlas/atlas.js'; +import { CssRuleExtractor } from './cssRuleExtractor.js'; import { fullFileRenderStrategyWgsl } from './fullFileRenderStrategy.wgsl.js'; import { BindingId, type IGpuRenderStrategy } from './gpu.js'; import { GPULifecycle } from './gpuDisposable.js'; @@ -48,6 +48,7 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend readonly wgsl: string = fullFileRenderStrategyWgsl; private readonly _glyphRasterizer: GlyphRasterizer; + private readonly _cssRuleExtractor: CssRuleExtractor; private _cellBindBuffer!: GPUBuffer; @@ -89,6 +90,7 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend const fontSize = this._context.configuration.options.get(EditorOption.fontSize); this._glyphRasterizer = this._register(new GlyphRasterizer(fontSize, fontFamily)); + this._cssRuleExtractor = this._register(new CssRuleExtractor(this._viewGpuContext)); const bufferSize = this._viewGpuContext.maxGpuLines * this._viewGpuContext.maxGpuCols * Constants.IndicesPerCell * Float32Array.BYTES_PER_ELEMENT; this._cellBindBuffer = this._register(GPULifecycle.createBuffer(this._device, { @@ -336,7 +338,7 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend // TODO: We'd want to optimize pulling the decorations in order // HACK: Temporary replace char to demonstrate inline decorations - const cellDecorations = decorations.filter(decoration => { + const cellDecorations = inlineDecorations.filter(decoration => { // TODO: Why does Range.containsPosition and Range.strictContainsPosition not work here? if (y < decoration.range.startLineNumber || y > decoration.range.endLineNumber) { return false; @@ -350,6 +352,25 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend return true; }); for (const decoration of cellDecorations) { + if (!decoration.options.inlineClassName) { + throw new BugIndicatingError('Expected inlineClassName on decoration'); + } + const rules = this._cssRuleExtractor.getStyleRules(decoration.options.inlineClassName); + const supportedCssRules = [ + 'text-decoration-line', + 'text-decoration-thickness', + 'text-decoration-style', + 'text-decoration-color', + ]; + const supported = rules.every(rule => { + for (const r of rule.style) { + if (!supportedCssRules.includes(r)) { + return false; + } + } + return true; + }); + console.log('rules supported?', supported, rules); switch (decoration.options.inlineClassName) { case (ClassName.EditorDeprecatedInlineDecoration): { // HACK: We probably shouldn't override tokenMetadata From 2f88b4b368555e9aec76df7d3a6e3e7ebae5603a Mon Sep 17 00:00:00 2001 From: Osvaldo Ortega Date: Tue, 19 Nov 2024 11:22:52 -0800 Subject: [PATCH 003/119] Find widget search history per editor instance --- src/vs/editor/common/config/editorOptions.ts | 7 ++++--- .../editor/contrib/find/browser/findWidget.ts | 8 ++++++-- .../find/browser/findWidgetSearchHistory.ts | 18 +++++++++++++++--- src/vs/monaco.d.ts | 2 +- 4 files changed, 26 insertions(+), 9 deletions(-) diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index 9e50bfd86edc5..409a94bbc9420 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -1658,7 +1658,7 @@ export interface IEditorFindOptions { /** * Controls how the find widget search history should be stored */ - findSearchHistory?: 'never' | 'workspace'; + findSearchHistory?: 'never' | 'workspace' | 'editor'; } /** @@ -1726,11 +1726,12 @@ class EditorFind extends BaseEditorOption(input.findSearchHistory, this.defaultValue.findSearchHistory, ['never', 'workspace']), + findSearchHistory: stringSet<'never' | 'workspace' | 'editor'>(input.findSearchHistory, this.defaultValue.findSearchHistory, ['never', 'workspace', 'editor']), }; } } diff --git a/src/vs/editor/contrib/find/browser/findWidget.ts b/src/vs/editor/contrib/find/browser/findWidget.ts index b3a6a04e3a14c..dcbd6037aba2b 100644 --- a/src/vs/editor/contrib/find/browser/findWidget.ts +++ b/src/vs/editor/contrib/find/browser/findWidget.ts @@ -186,7 +186,11 @@ export class FindWidget extends Widget implements IOverlayWidget, IVerticalSashL this._contextKeyService = contextKeyService; this._storageService = storageService; this._notificationService = notificationService; - this._findWidgetSearchHistory = new FindWidgetSearchHistory(this._storageService); + const findSearchHistoryConfig = this._codeEditor.getOption(EditorOption.find).findSearchHistory; + this._findWidgetSearchHistory = new FindWidgetSearchHistory( + this._storageService, + findSearchHistoryConfig === 'editor' ? this._codeEditor : undefined + ); this._ctrlEnterReplaceAllWarningPrompted = !!storageService.getBoolean(ctrlEnterReplaceAllWarningPromptedKey, StorageScope.PROFILE); @@ -970,7 +974,7 @@ export class FindWidget extends Widget implements IOverlayWidget, IVerticalSashL showHistoryHint: () => showHistoryKeybindingHint(this._keybindingService), inputBoxStyles: defaultInputBoxStyles, toggleStyles: defaultToggleStyles, - history: findSearchHistoryConfig === 'workspace' ? this._findWidgetSearchHistory : new Set([]), + history: findSearchHistoryConfig === 'never' ? new Set([]) : this._findWidgetSearchHistory, }, this._contextKeyService)); this._findInput.setRegex(!!this._state.isRegex); this._findInput.setCaseSensitive(!!this._state.matchCase); diff --git a/src/vs/editor/contrib/find/browser/findWidgetSearchHistory.ts b/src/vs/editor/contrib/find/browser/findWidgetSearchHistory.ts index 0dd6f0753a1ac..21baea970f6fe 100644 --- a/src/vs/editor/contrib/find/browser/findWidgetSearchHistory.ts +++ b/src/vs/editor/contrib/find/browser/findWidgetSearchHistory.ts @@ -5,15 +5,27 @@ import { IHistory } from '../../../../base/common/history.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; +import { ICodeEditor } from '../../../browser/editorBrowser.js'; export class FindWidgetSearchHistory implements IHistory { - public static readonly FIND_HISTORY_KEY = 'workbench.find.history'; + private static readonly FIND_HISTORY_KEY = 'workbench.find.history'; + private readonly id: string; private inMemoryValues: Set = new Set(); constructor( @IStorageService private readonly storageService: IStorageService, + codeEditor?: ICodeEditor, ) { this.load(); + if (codeEditor) { + this.id = `${FindWidgetSearchHistory.FIND_HISTORY_KEY}.${codeEditor.getId()}`; + // The editor id could be re-used, so we need to clean the storage when it gets disposed + codeEditor.onDidDispose(() => { + this.clear(); + }); + } else { + this.id = FindWidgetSearchHistory.FIND_HISTORY_KEY; + } } delete(t: string): boolean { @@ -50,7 +62,7 @@ export class FindWidgetSearchHistory implements IHistory { load() { let result: [] | undefined; const raw = this.storageService.get( - FindWidgetSearchHistory.FIND_HISTORY_KEY, + this.id, StorageScope.WORKSPACE ); @@ -71,7 +83,7 @@ export class FindWidgetSearchHistory implements IHistory { this.inMemoryValues.forEach(e => elements.push(e)); return new Promise(resolve => { this.storageService.store( - FindWidgetSearchHistory.FIND_HISTORY_KEY, + this.id, JSON.stringify(elements), StorageScope.WORKSPACE, StorageTarget.USER, diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 11b8f7415dcc2..77bb8ebe83373 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -4064,7 +4064,7 @@ declare namespace monaco.editor { /** * Controls how the find widget search history should be stored */ - findSearchHistory?: 'never' | 'workspace'; + findSearchHistory?: 'never' | 'workspace' | 'editor'; } export type GoToLocationValues = 'peek' | 'gotoAndPeek' | 'goto'; From a42e4e9a5f923e10a4af8506502ad8041905e464 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 19 Nov 2024 12:13:49 -0800 Subject: [PATCH 004/119] Improve rule pulling, report unsupported rules --- src/vs/editor/browser/gpu/cssRuleExtractor.ts | 77 ------------------- .../browser/gpu/decorationCssRuleExtractor.ts | 57 ++++++++++++++ .../browser/gpu/fullFileRenderStrategy.ts | 29 ++----- src/vs/editor/browser/gpu/viewGpuContext.ts | 66 ++++++++++++++-- .../browser/viewParts/gpuMark/gpuMark.ts | 4 +- .../browser/viewParts/viewLines/viewLine.ts | 4 +- .../viewParts/viewLinesGpu/viewLinesGpu.ts | 4 +- 7 files changed, 128 insertions(+), 113 deletions(-) delete mode 100644 src/vs/editor/browser/gpu/cssRuleExtractor.ts create mode 100644 src/vs/editor/browser/gpu/decorationCssRuleExtractor.ts diff --git a/src/vs/editor/browser/gpu/cssRuleExtractor.ts b/src/vs/editor/browser/gpu/cssRuleExtractor.ts deleted file mode 100644 index d8bd41ac385d7..0000000000000 --- a/src/vs/editor/browser/gpu/cssRuleExtractor.ts +++ /dev/null @@ -1,77 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { $, getActiveDocument } from '../../../base/browser/dom.js'; -import { Disposable, toDisposable } from '../../../base/common/lifecycle.js'; -import type { ViewGpuContext } from './viewGpuContext.js'; - -export class CssRuleExtractor extends Disposable { - private _container: HTMLElement; - - private _ruleCache: Map = new Map(); - - constructor( - private readonly _viewGpuContext: ViewGpuContext, - ) { - super(); - - this._container = $('div.monaco-css-rule-extractor'); - this._container.style.visibility = 'hidden'; - const parentElement = this._viewGpuContext.canvas.domNode.parentElement; - if (!parentElement) { - throw new Error('No parent element found for the canvas'); - } - parentElement.appendChild(this._container); - this._register(toDisposable(() => this._container.remove())); - } - - getStyleRules(className: string): CSSStyleRule[] { - const existing = this._ruleCache.get(className); - if (existing) { - return existing; - } - const dummyElement = $(`span.${className}`); - this._container.appendChild(dummyElement); - const rules = this._getStyleRules(dummyElement, className); - this._ruleCache.set(className, rules); - return rules; - } - - private _getStyleRules(element: HTMLElement, className: string) { - const matchedRules = []; - - // Iterate through all stylesheets - const doc = getActiveDocument(); - for (const stylesheet of doc.styleSheets) { - try { - // Iterate through all CSS rules in the stylesheet - for (const rule of stylesheet.cssRules) { - if (rule instanceof CSSImportRule) { - // Recursively process the import rule - if (rule.styleSheet?.cssRules) { - for (const innerRule of rule.styleSheet.cssRules) { - if (innerRule instanceof CSSStyleRule) { - if (element.matches(innerRule.selectorText) && innerRule.selectorText.includes(className)) { - matchedRules.push(innerRule); - } - } - } - } - } else if (rule instanceof CSSStyleRule) { - // Check if the element matches the selector - if (element.matches(rule.selectorText) && rule.selectorText.includes(className)) { - matchedRules.push(rule); - } - } - } - } catch (e) { - // Some stylesheets may not be accessible due to CORS restrictions - console.warn('Could not access stylesheet:', stylesheet.href); - } - } - - return matchedRules; - } -} diff --git a/src/vs/editor/browser/gpu/decorationCssRuleExtractor.ts b/src/vs/editor/browser/gpu/decorationCssRuleExtractor.ts new file mode 100644 index 0000000000000..5959bd36cd3e6 --- /dev/null +++ b/src/vs/editor/browser/gpu/decorationCssRuleExtractor.ts @@ -0,0 +1,57 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { $, getActiveDocument } from '../../../base/browser/dom.js'; +import { Disposable, toDisposable } from '../../../base/common/lifecycle.js'; + +export class DecorationCssRuleExtractor extends Disposable { + private _container: HTMLElement; + + private _ruleCache: Map = new Map(); + + constructor() { + super(); + this._container = $('div.monaco-css-rule-extractor'); + this._container.style.visibility = 'hidden'; + this._register(toDisposable(() => this._container.remove())); + } + + getStyleRules(canvas: HTMLElement, decorationClassName: string): CSSStyleRule[] { + const existing = this._ruleCache.get(decorationClassName); + if (existing) { + return existing; + } + const dummyElement = $(`span.${decorationClassName}`); + this._container.appendChild(dummyElement); + const rules = this._getStyleRules(canvas, dummyElement, decorationClassName); + this._ruleCache.set(decorationClassName, rules); + return rules; + } + + private _getStyleRules(canvas: HTMLElement, element: HTMLElement, className: string) { + canvas.appendChild(this._container); + + // Iterate through all stylesheets and imported stylesheets to find matching rules + const rules = []; + const doc = getActiveDocument(); + const stylesheets = [...doc.styleSheets]; + for (let i = 0; i < stylesheets.length; i++) { + const stylesheet = stylesheets[i]; + for (const rule of stylesheet.cssRules) { + if (rule instanceof CSSImportRule) { + if (rule.styleSheet) { + stylesheets.push(rule.styleSheet); + } + } else if (rule instanceof CSSStyleRule) { + if (element.matches(rule.selectorText) && rule.selectorText.includes(`.${className}`)) { + rules.push(rule); + } + } + } + } + + return rules; + } +} diff --git a/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts b/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts index a57f6c9f085fe..473e662a37b64 100644 --- a/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts +++ b/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { getActiveDocument, getActiveWindow } from '../../../base/browser/dom.js'; +import { getActiveWindow } from '../../../base/browser/dom.js'; import { BugIndicatingError } from '../../../base/common/errors.js'; import { EditorOption } from '../../common/config/editorOptions.js'; import { CursorColumns } from '../../common/core/cursorColumns.js'; @@ -17,7 +17,6 @@ import type { ViewLineRenderingData } from '../../common/viewModel.js'; import type { ViewContext } from '../../common/viewModel/viewContext.js'; import type { ViewLineOptions } from '../viewParts/viewLines/viewLineOptions.js'; import type { ITextureAtlasPageGlyph } from './atlas/atlas.js'; -import { CssRuleExtractor } from './cssRuleExtractor.js'; import { fullFileRenderStrategyWgsl } from './fullFileRenderStrategy.wgsl.js'; import { BindingId, type IGpuRenderStrategy } from './gpu.js'; import { GPULifecycle } from './gpuDisposable.js'; @@ -48,7 +47,6 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend readonly wgsl: string = fullFileRenderStrategyWgsl; private readonly _glyphRasterizer: GlyphRasterizer; - private readonly _cssRuleExtractor: CssRuleExtractor; private _cellBindBuffer!: GPUBuffer; @@ -90,7 +88,6 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend const fontSize = this._context.configuration.options.get(EditorOption.fontSize); this._glyphRasterizer = this._register(new GlyphRasterizer(fontSize, fontFamily)); - this._cssRuleExtractor = this._register(new CssRuleExtractor(this._viewGpuContext)); const bufferSize = this._viewGpuContext.maxGpuLines * this._viewGpuContext.maxGpuCols * Constants.IndicesPerCell * Float32Array.BYTES_PER_ELEMENT; this._cellBindBuffer = this._register(GPULifecycle.createBuffer(this._device, { @@ -291,7 +288,7 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend for (y = viewportData.startLineNumber; y <= viewportData.endLineNumber; y++) { // Only attempt to render lines that the GPU renderer can handle - if (!ViewGpuContext.canRender(viewLineOptions, viewportData, y)) { + if (!ViewGpuContext.canRender(this._viewGpuContext.canvas.domNode, viewLineOptions, viewportData, y)) { fillStartIndex = ((y - 1) * this._viewGpuContext.maxGpuCols) * Constants.IndicesPerCell; fillEndIndex = (y * this._viewGpuContext.maxGpuCols) * Constants.IndicesPerCell; cellBuffer.fill(0, fillStartIndex, fillEndIndex); @@ -351,26 +348,10 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend } return true; }); + + // Only lines containing fully supported inline decorations should have made it + // this far. for (const decoration of cellDecorations) { - if (!decoration.options.inlineClassName) { - throw new BugIndicatingError('Expected inlineClassName on decoration'); - } - const rules = this._cssRuleExtractor.getStyleRules(decoration.options.inlineClassName); - const supportedCssRules = [ - 'text-decoration-line', - 'text-decoration-thickness', - 'text-decoration-style', - 'text-decoration-color', - ]; - const supported = rules.every(rule => { - for (const r of rule.style) { - if (!supportedCssRules.includes(r)) { - return false; - } - } - return true; - }); - console.log('rules supported?', supported, rules); switch (decoration.options.inlineClassName) { case (ClassName.EditorDeprecatedInlineDecoration): { // HACK: We probably shouldn't override tokenMetadata diff --git a/src/vs/editor/browser/gpu/viewGpuContext.ts b/src/vs/editor/browser/gpu/viewGpuContext.ts index 310a6f97f5d1e..70aff96eaaa33 100644 --- a/src/vs/editor/browser/gpu/viewGpuContext.ts +++ b/src/vs/editor/browser/gpu/viewGpuContext.ts @@ -19,7 +19,7 @@ import { GPULifecycle } from './gpuDisposable.js'; import { ensureNonNullable, observeDevicePixelDimensions } from './gpuUtils.js'; import { RectangleRenderer } from './rectangleRenderer.js'; import type { ViewContext } from '../../common/viewModel/viewContext.js'; -import { ClassName } from '../../common/model/intervalTree.js'; +import { DecorationCssRuleExtractor } from './decorationCssRuleExtractor.js'; const enum GpuRenderLimits { maxGpuLines = 3000, @@ -48,6 +48,7 @@ export class ViewGpuContext extends Disposable { private static _atlas: TextureAtlas | undefined; + private static readonly _decorationCssRuleExtractor = new DecorationCssRuleExtractor(); /** * The shared texture atlas to use across all views. @@ -126,25 +127,52 @@ export class ViewGpuContext extends Disposable { * renderer. Eventually this should trend all lines, except maybe exceptional cases like * decorations that use class names. */ - public static canRender(options: ViewLineOptions, viewportData: ViewportData, lineNumber: number): boolean { + public static canRender(container: HTMLElement, options: ViewLineOptions, viewportData: ViewportData, lineNumber: number): boolean { const data = viewportData.getViewLineRenderingData(lineNumber); + + // Check if the line has simple attributes that aren't supported if ( data.containsRTL || data.maxColumn > GpuRenderLimits.maxGpuCols || data.continuesWithWrappedLine || - // HACK: ... - data.inlineDecorations.length > 0 && data.inlineDecorations[0].inlineClassName !== ClassName.EditorDeprecatedInlineDecoration || lineNumber >= GpuRenderLimits.maxGpuLines ) { return false; } + + // Check if all inline decorations are supported + if (data.inlineDecorations.length > 0) { + let supported = true; + for (const decoration of data.inlineDecorations) { + const styleRules = ViewGpuContext._decorationCssRuleExtractor.getStyleRules(container, decoration.inlineClassName); + const supportedCssRules = [ + 'text-decoration-line', + 'text-decoration-thickness', + 'text-decoration-style', + 'text-decoration-color', + ]; + supported &&= styleRules.every(rule => { + for (const r of rule.style) { + if (!supportedCssRules.includes(r)) { + return false; + } + } + return true; + }); + if (!supported) { + break; + } + } + return supported; + } + return true; } /** * Like {@link canRender} but returned detailed information about why the line cannot be rendered. */ - public static canRenderDetailed(options: ViewLineOptions, viewportData: ViewportData, lineNumber: number): string[] { + public static canRenderDetailed(container: HTMLElement, options: ViewLineOptions, viewportData: ViewportData, lineNumber: number): string[] { const data = viewportData.getViewLineRenderingData(lineNumber); const reasons: string[] = []; if (data.containsRTL) { @@ -157,9 +185,31 @@ export class ViewGpuContext extends Disposable { reasons.push('continuesWithWrappedLine'); } if (data.inlineDecorations.length > 0) { - // HACK: ... - if (data.inlineDecorations[0].inlineClassName !== ClassName.EditorDeprecatedInlineDecoration) { - reasons.push('inlineDecorations > 0'); + let supported = true; + const problemRules: string[] = []; + for (const decoration of data.inlineDecorations) { + const styleRules = ViewGpuContext._decorationCssRuleExtractor.getStyleRules(container, decoration.inlineClassName); + const supportedCssRules = [ + 'text-decoration-line', + 'text-decoration-thickness', + 'text-decoration-style', + 'text-decoration-color', + ]; + supported &&= styleRules.every(rule => { + for (const r of rule.style) { + if (!supportedCssRules.includes(r)) { + problemRules.push(r); + return false; + } + } + return true; + }); + if (!supported) { + break; + } + } + if (problemRules.length > 0) { + reasons.push(`inlineDecorations with unsupported CSS rules (\`${problemRules.join(', ')}\`)`); } } if (lineNumber >= GpuRenderLimits.maxGpuLines) { diff --git a/src/vs/editor/browser/viewParts/gpuMark/gpuMark.ts b/src/vs/editor/browser/viewParts/gpuMark/gpuMark.ts index 7019eff5e007e..861ff529cdc47 100644 --- a/src/vs/editor/browser/viewParts/gpuMark/gpuMark.ts +++ b/src/vs/editor/browser/viewParts/gpuMark/gpuMark.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { getActiveDocument } from '../../../../base/browser/dom.js'; import * as viewEvents from '../../../common/viewEvents.js'; import { ViewContext } from '../../../common/viewModel/viewContext.js'; import { ViewGpuContext } from '../../gpu/viewGpuContext.js'; @@ -77,7 +78,8 @@ export class GpuMarkOverlay extends DynamicViewOverlay { const output: string[] = []; for (let lineNumber = visibleStartLineNumber; lineNumber <= visibleEndLineNumber; lineNumber++) { const lineIndex = lineNumber - visibleStartLineNumber; - const cannotRenderReasons = ViewGpuContext.canRenderDetailed(options, viewportData, lineNumber); + // TODO: How to get the container? + const cannotRenderReasons = ViewGpuContext.canRenderDetailed(getActiveDocument().querySelector('.view-lines')!, options, viewportData, lineNumber); output[lineIndex] = cannotRenderReasons.length ? `
` : ''; } diff --git a/src/vs/editor/browser/viewParts/viewLines/viewLine.ts b/src/vs/editor/browser/viewParts/viewLines/viewLine.ts index 5e555246cea86..d25238bb4cf54 100644 --- a/src/vs/editor/browser/viewParts/viewLines/viewLine.ts +++ b/src/vs/editor/browser/viewParts/viewLines/viewLine.ts @@ -19,6 +19,7 @@ import { EditorFontLigatures } from '../../../common/config/editorOptions.js'; import { DomReadingContext } from './domReadingContext.js'; import type { ViewLineOptions } from './viewLineOptions.js'; import { ViewGpuContext } from '../../gpu/viewGpuContext.js'; +import { getActiveDocument } from '../../../../base/browser/dom.js'; const canUseFastRenderedViewLine = (function () { if (platform.isNative) { @@ -98,7 +99,8 @@ export class ViewLine implements IVisibleLine { } public renderLine(lineNumber: number, deltaTop: number, lineHeight: number, viewportData: ViewportData, sb: StringBuilder): boolean { - if (this._options.useGpu && ViewGpuContext.canRender(this._options, viewportData, lineNumber)) { + // TODO: How to get the container? + if (this._options.useGpu && ViewGpuContext.canRender(getActiveDocument().querySelector('.view-lines')!, this._options, viewportData, lineNumber)) { this._renderedViewLine?.domNode?.domNode.remove(); this._renderedViewLine = null; return false; diff --git a/src/vs/editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts b/src/vs/editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts index c4d8c0400d214..cfe4a974bd78f 100644 --- a/src/vs/editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts +++ b/src/vs/editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts @@ -551,7 +551,7 @@ export class ViewLinesGpu extends ViewPart implements IViewLines { if (!this._lastViewportData || !this._lastViewLineOptions) { return undefined; } - if (!ViewGpuContext.canRender(this._lastViewLineOptions, this._lastViewportData, lineNumber)) { + if (!ViewGpuContext.canRender(this._viewGpuContext.canvas.domNode, this._lastViewLineOptions, this._lastViewportData, lineNumber)) { return undefined; } @@ -569,7 +569,7 @@ export class ViewLinesGpu extends ViewPart implements IViewLines { if (!this._lastViewportData || !this._lastViewLineOptions) { return undefined; } - if (!ViewGpuContext.canRender(this._lastViewLineOptions, this._lastViewportData, lineNumber)) { + if (!ViewGpuContext.canRender(this._viewGpuContext.canvas.domNode, this._lastViewLineOptions, this._lastViewportData, lineNumber)) { return undefined; } const lineData = this._lastViewportData.getViewLineRenderingData(lineNumber); From f536f763b2ac08658fd0c7473325e42710083776 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 19 Nov 2024 12:33:24 -0800 Subject: [PATCH 005/119] Apply CSS styles in gpu lines --- .../browser/gpu/fullFileRenderStrategy.ts | 32 ++++++++++++++++--- src/vs/editor/browser/gpu/viewGpuContext.ts | 31 +++++++++--------- 2 files changed, 42 insertions(+), 21 deletions(-) diff --git a/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts b/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts index 473e662a37b64..2969cbe23ea53 100644 --- a/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts +++ b/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts @@ -8,7 +8,6 @@ import { BugIndicatingError } from '../../../base/common/errors.js'; import { EditorOption } from '../../common/config/editorOptions.js'; import { CursorColumns } from '../../common/core/cursorColumns.js'; import { MetadataConsts } from '../../common/encodedTokenAttributes.js'; -import { ClassName } from '../../common/model/intervalTree.js'; import type { IViewLineTokens } from '../../common/tokens/lineTokens.js'; import { ViewEventHandler } from '../../common/viewEventHandler.js'; import { ViewEventType, type ViewConfigurationChangedEvent, type ViewDecorationsChangedEvent, type ViewLinesChangedEvent, type ViewLinesDeletedEvent, type ViewLinesInsertedEvent, type ViewScrollChangedEvent, type ViewTokensChangedEvent, type ViewZonesChangedEvent } from '../../common/viewEvents.js'; @@ -351,14 +350,37 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend // Only lines containing fully supported inline decorations should have made it // this far. + const inlineStyles: Map = new Map(); for (const decoration of cellDecorations) { - switch (decoration.options.inlineClassName) { - case (ClassName.EditorDeprecatedInlineDecoration): { - // HACK: We probably shouldn't override tokenMetadata + if (!decoration.options.inlineClassName) { + throw new BugIndicatingError('Unexpected inline decoration without class name'); + } + const rules = ViewGpuContext.decorationCssRuleExtractor.getStyleRules(this._viewGpuContext.canvas.domNode, decoration.options.inlineClassName); + for (const rule of rules) { + for (const r of rule.style) { + inlineStyles.set(r, rule.styleMap.get(r)?.toString() ?? ''); + } + } + } + + for (const [k, v] of inlineStyles.entries()) { + switch (k) { + case 'text-decoration-line': { + // TODO: Don't set tokenMetadata as it applies to more than just this token tokenMetadata |= MetadataConsts.STRIKETHROUGH_MASK; - // chars = '-'; break; } + case 'text-decoration-thickness': + case 'text-decoration-style': + case 'text-decoration-color': { + // HACK: Ignore for now to avoid throwing + break; + } + // case 'color': { + // tokenMetadata |= ... + // break; + // } + default: throw new BugIndicatingError('Unexpected inline decoration style'); } } diff --git a/src/vs/editor/browser/gpu/viewGpuContext.ts b/src/vs/editor/browser/gpu/viewGpuContext.ts index 70aff96eaaa33..ae4264cb24438 100644 --- a/src/vs/editor/browser/gpu/viewGpuContext.ts +++ b/src/vs/editor/browser/gpu/viewGpuContext.ts @@ -46,9 +46,12 @@ export class ViewGpuContext extends Disposable { readonly rectangleRenderer: RectangleRenderer; - private static _atlas: TextureAtlas | undefined; - private static readonly _decorationCssRuleExtractor = new DecorationCssRuleExtractor(); + static get decorationCssRuleExtractor(): DecorationCssRuleExtractor { + return ViewGpuContext._decorationCssRuleExtractor; + } + + private static _atlas: TextureAtlas | undefined; /** * The shared texture atlas to use across all views. @@ -145,15 +148,9 @@ export class ViewGpuContext extends Disposable { let supported = true; for (const decoration of data.inlineDecorations) { const styleRules = ViewGpuContext._decorationCssRuleExtractor.getStyleRules(container, decoration.inlineClassName); - const supportedCssRules = [ - 'text-decoration-line', - 'text-decoration-thickness', - 'text-decoration-style', - 'text-decoration-color', - ]; supported &&= styleRules.every(rule => { for (const r of rule.style) { - if (!supportedCssRules.includes(r)) { + if (!gpuSupportedCssRules.includes(r)) { return false; } } @@ -189,15 +186,9 @@ export class ViewGpuContext extends Disposable { const problemRules: string[] = []; for (const decoration of data.inlineDecorations) { const styleRules = ViewGpuContext._decorationCssRuleExtractor.getStyleRules(container, decoration.inlineClassName); - const supportedCssRules = [ - 'text-decoration-line', - 'text-decoration-thickness', - 'text-decoration-style', - 'text-decoration-color', - ]; supported &&= styleRules.every(rule => { for (const r of rule.style) { - if (!supportedCssRules.includes(r)) { + if (!gpuSupportedCssRules.includes(r)) { problemRules.push(r); return false; } @@ -218,3 +209,11 @@ export class ViewGpuContext extends Disposable { return reasons; } } + +const gpuSupportedCssRules = [ + // 'color', + 'text-decoration-line', + 'text-decoration-thickness', + 'text-decoration-style', + 'text-decoration-color', +]; From ee5b89187745d866cf849ff5d2e78e1bb39b5b72 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 19 Nov 2024 13:56:50 -0800 Subject: [PATCH 006/119] Apply style to char metadata, not token --- .../browser/gpu/fullFileRenderStrategy.ts | 22 +++++++++++-------- src/vs/editor/browser/gpu/viewGpuContext.ts | 10 ++++----- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts b/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts index 2969cbe23ea53..d9f053c3a1a44 100644 --- a/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts +++ b/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts @@ -225,6 +225,8 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend let tokenEndIndex = 0; let tokenMetadata = 0; + let charMetadata = 0; + let lineData: ViewLineRenderingData; let content: string = ''; let fillStartIndex = 0; @@ -331,6 +333,7 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend break; } chars = content.charAt(x); + charMetadata = tokenMetadata; // TODO: We'd want to optimize pulling the decorations in order // HACK: Temporary replace char to demonstrate inline decorations @@ -363,11 +366,10 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend } } - for (const [k, v] of inlineStyles.entries()) { - switch (k) { + for (const [key, _value] of inlineStyles.entries()) { + switch (key) { case 'text-decoration-line': { - // TODO: Don't set tokenMetadata as it applies to more than just this token - tokenMetadata |= MetadataConsts.STRIKETHROUGH_MASK; + charMetadata |= MetadataConsts.STRIKETHROUGH_MASK; break; } case 'text-decoration-thickness': @@ -376,10 +378,12 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend // HACK: Ignore for now to avoid throwing break; } - // case 'color': { - // tokenMetadata |= ... - // break; - // } + case 'color': { + // HACK: Set color requests to the first token's fg color + charMetadata &= ~MetadataConsts.FOREGROUND_MASK; + charMetadata |= 0b1 << MetadataConsts.FOREGROUND_OFFSET; + break; + } default: throw new BugIndicatingError('Unexpected inline decoration style'); } } @@ -395,7 +399,7 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend continue; } - glyph = this._viewGpuContext.atlas.getGlyph(this._glyphRasterizer, chars, tokenMetadata); + glyph = this._viewGpuContext.atlas.getGlyph(this._glyphRasterizer, chars, charMetadata); // TODO: Support non-standard character widths absoluteOffsetX = Math.round((x + xOffset) * viewLineOptions.spaceWidth * dpr); diff --git a/src/vs/editor/browser/gpu/viewGpuContext.ts b/src/vs/editor/browser/gpu/viewGpuContext.ts index ae4264cb24438..162139b3cb82b 100644 --- a/src/vs/editor/browser/gpu/viewGpuContext.ts +++ b/src/vs/editor/browser/gpu/viewGpuContext.ts @@ -211,9 +211,9 @@ export class ViewGpuContext extends Disposable { } const gpuSupportedCssRules = [ - // 'color', - 'text-decoration-line', - 'text-decoration-thickness', - 'text-decoration-style', - 'text-decoration-color', + 'color', + // 'text-decoration-line', + // 'text-decoration-thickness', + // 'text-decoration-style', + // 'text-decoration-color', ]; From adaa9f2c51f44bc71372d0d258f1268b20b1d01f Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 20 Nov 2024 07:16:16 -0800 Subject: [PATCH 007/119] Start of gpu character metadata concept --- src/vs/editor/browser/gpu/atlas/atlas.ts | 4 +-- .../editor/browser/gpu/atlas/textureAtlas.ts | 34 +++++++++---------- .../browser/gpu/atlas/textureAtlasPage.ts | 16 ++++----- .../browser/gpu/fullFileRenderStrategy.ts | 22 ++++++++---- .../browser/gpu/raster/glyphRasterizer.ts | 19 +++++++---- src/vs/editor/browser/gpu/raster/raster.ts | 16 +++++++-- src/vs/editor/browser/gpu/viewGpuContext.ts | 1 + 7 files changed, 70 insertions(+), 42 deletions(-) diff --git a/src/vs/editor/browser/gpu/atlas/atlas.ts b/src/vs/editor/browser/gpu/atlas/atlas.ts index e971904270525..a8a2fcee9aac1 100644 --- a/src/vs/editor/browser/gpu/atlas/atlas.ts +++ b/src/vs/editor/browser/gpu/atlas/atlas.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import type { ThreeKeyMap } from '../../../../base/common/map.js'; +import type { FourKeyMap } from '../../../../base/common/map.js'; import type { IBoundingBox, IRasterizedGlyph } from '../raster/raster.js'; /** @@ -92,4 +92,4 @@ export const enum UsagePreviewColors { Restricted = '#FF000088', } -export type GlyphMap = ThreeKeyMap; +export type GlyphMap = FourKeyMap; diff --git a/src/vs/editor/browser/gpu/atlas/textureAtlas.ts b/src/vs/editor/browser/gpu/atlas/textureAtlas.ts index fb9ac44f4a8ec..2d4365da93bbb 100644 --- a/src/vs/editor/browser/gpu/atlas/textureAtlas.ts +++ b/src/vs/editor/browser/gpu/atlas/textureAtlas.ts @@ -8,7 +8,7 @@ import { CharCode } from '../../../../base/common/charCode.js'; import { BugIndicatingError } from '../../../../base/common/errors.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { Disposable, dispose, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; -import { ThreeKeyMap } from '../../../../base/common/map.js'; +import { FourKeyMap } from '../../../../base/common/map.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { IThemeService } from '../../../../platform/theme/common/themeService.js'; import { MetadataConsts } from '../../../common/encodedTokenAttributes.js'; @@ -44,7 +44,7 @@ export class TextureAtlas extends Disposable { * so it is not guaranteed to be the actual page the glyph is on. But it is guaranteed that all * pages with a lower index do not contain the glyph. */ - private readonly _glyphPageIndex: GlyphMap = new ThreeKeyMap(); + private readonly _glyphPageIndex: GlyphMap = new FourKeyMap(); private readonly _onDidDeleteGlyphs = this._register(new Emitter()); readonly onDidDeleteGlyphs = this._onDidDeleteGlyphs.event; @@ -83,7 +83,7 @@ export class TextureAtlas extends Disposable { // cells end up rendering nothing // TODO: This currently means the first slab is for 0x0 glyphs and is wasted const nullRasterizer = new GlyphRasterizer(1, ''); - firstPage.getGlyph(nullRasterizer, '', 0); + firstPage.getGlyph(nullRasterizer, '', 0, 0); nullRasterizer.dispose(); } @@ -104,10 +104,10 @@ export class TextureAtlas extends Disposable { this._onDidDeleteGlyphs.fire(); } - getGlyph(rasterizer: IGlyphRasterizer, chars: string, metadata: number): Readonly { + getGlyph(rasterizer: IGlyphRasterizer, chars: string, tokenMetadata: number, charMetadata: number): Readonly { // TODO: Encode font size and family into key // Ignore metadata that doesn't affect the glyph - metadata &= ~(MetadataConsts.LANGUAGEID_MASK | MetadataConsts.TOKEN_TYPE_MASK | MetadataConsts.BALANCED_BRACKETS_MASK); + tokenMetadata &= ~(MetadataConsts.LANGUAGEID_MASK | MetadataConsts.TOKEN_TYPE_MASK | MetadataConsts.BALANCED_BRACKETS_MASK); // Warm up common glyphs if (!this._warmedUpRasterizers.has(rasterizer.id)) { @@ -116,25 +116,25 @@ export class TextureAtlas extends Disposable { } // Try get the glyph, overflowing to a new page if necessary - return this._tryGetGlyph(this._glyphPageIndex.get(chars, metadata, rasterizer.cacheKey) ?? 0, rasterizer, chars, metadata); + return this._tryGetGlyph(this._glyphPageIndex.get(chars, tokenMetadata, charMetadata, rasterizer.cacheKey) ?? 0, rasterizer, chars, tokenMetadata, charMetadata); } - private _tryGetGlyph(pageIndex: number, rasterizer: IGlyphRasterizer, chars: string, metadata: number): Readonly { - this._glyphPageIndex.set(chars, metadata, rasterizer.cacheKey, pageIndex); + private _tryGetGlyph(pageIndex: number, rasterizer: IGlyphRasterizer, chars: string, tokenMetadata: number, charMetadata: number): Readonly { + this._glyphPageIndex.set(chars, tokenMetadata, charMetadata, rasterizer.cacheKey, pageIndex); return ( - this._pages[pageIndex].getGlyph(rasterizer, chars, metadata) + this._pages[pageIndex].getGlyph(rasterizer, chars, tokenMetadata, charMetadata) ?? (pageIndex + 1 < this._pages.length - ? this._tryGetGlyph(pageIndex + 1, rasterizer, chars, metadata) + ? this._tryGetGlyph(pageIndex + 1, rasterizer, chars, tokenMetadata, charMetadata) : undefined) - ?? this._getGlyphFromNewPage(rasterizer, chars, metadata) + ?? this._getGlyphFromNewPage(rasterizer, chars, tokenMetadata, charMetadata) ); } - private _getGlyphFromNewPage(rasterizer: IGlyphRasterizer, chars: string, metadata: number): Readonly { + private _getGlyphFromNewPage(rasterizer: IGlyphRasterizer, chars: string, tokenMetadata: number, charMetadata: number): Readonly { // TODO: Support more than 2 pages and the GPU texture layer limit this._pages.push(this._instantiationService.createInstance(TextureAtlasPage, this._pages.length, this.pageSize, this._allocatorType)); - this._glyphPageIndex.set(chars, metadata, rasterizer.cacheKey, this._pages.length - 1); - return this._pages[this._pages.length - 1].getGlyph(rasterizer, chars, metadata)!; + this._glyphPageIndex.set(chars, tokenMetadata, charMetadata, rasterizer.cacheKey, this._pages.length - 1); + return this._pages[this._pages.length - 1].getGlyph(rasterizer, chars, tokenMetadata, charMetadata)!; } public getUsagePreview(): Promise { @@ -161,7 +161,7 @@ export class TextureAtlas extends Disposable { for (let code = CharCode.A; code <= CharCode.Z; code++) { taskQueue.enqueue(() => { for (const fgColor of colorMap.keys()) { - this.getGlyph(rasterizer, String.fromCharCode(code), (fgColor << MetadataConsts.FOREGROUND_OFFSET) & MetadataConsts.FOREGROUND_MASK); + this.getGlyph(rasterizer, String.fromCharCode(code), (fgColor << MetadataConsts.FOREGROUND_OFFSET) & MetadataConsts.FOREGROUND_MASK, 0); } }); } @@ -169,7 +169,7 @@ export class TextureAtlas extends Disposable { for (let code = CharCode.a; code <= CharCode.z; code++) { taskQueue.enqueue(() => { for (const fgColor of colorMap.keys()) { - this.getGlyph(rasterizer, String.fromCharCode(code), (fgColor << MetadataConsts.FOREGROUND_OFFSET) & MetadataConsts.FOREGROUND_MASK); + this.getGlyph(rasterizer, String.fromCharCode(code), (fgColor << MetadataConsts.FOREGROUND_OFFSET) & MetadataConsts.FOREGROUND_MASK, 0); } }); } @@ -177,7 +177,7 @@ export class TextureAtlas extends Disposable { for (let code = CharCode.ExclamationMark; code <= CharCode.Tilde; code++) { taskQueue.enqueue(() => { for (const fgColor of colorMap.keys()) { - this.getGlyph(rasterizer, String.fromCharCode(code), (fgColor << MetadataConsts.FOREGROUND_OFFSET) & MetadataConsts.FOREGROUND_MASK); + this.getGlyph(rasterizer, String.fromCharCode(code), (fgColor << MetadataConsts.FOREGROUND_OFFSET) & MetadataConsts.FOREGROUND_MASK, 0); } }); } diff --git a/src/vs/editor/browser/gpu/atlas/textureAtlasPage.ts b/src/vs/editor/browser/gpu/atlas/textureAtlasPage.ts index 01edf66913051..cbda9352a3bc8 100644 --- a/src/vs/editor/browser/gpu/atlas/textureAtlasPage.ts +++ b/src/vs/editor/browser/gpu/atlas/textureAtlasPage.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Disposable, toDisposable } from '../../../../base/common/lifecycle.js'; -import { ThreeKeyMap } from '../../../../base/common/map.js'; +import { FourKeyMap } from '../../../../base/common/map.js'; import { ILogService, LogLevel } from '../../../../platform/log/common/log.js'; import { IThemeService } from '../../../../platform/theme/common/themeService.js'; import type { IBoundingBox, IGlyphRasterizer } from '../raster/raster.js'; @@ -31,7 +31,7 @@ export class TextureAtlasPage extends Disposable implements IReadableTextureAtla private readonly _canvas: OffscreenCanvas; get source(): OffscreenCanvas { return this._canvas; } - private readonly _glyphMap: GlyphMap = new ThreeKeyMap(); + private readonly _glyphMap: GlyphMap = new FourKeyMap(); private readonly _glyphInOrderSet: Set = new Set(); get glyphs(): IterableIterator { return this._glyphInOrderSet.values(); @@ -65,20 +65,20 @@ export class TextureAtlasPage extends Disposable implements IReadableTextureAtla })); } - public getGlyph(rasterizer: IGlyphRasterizer, chars: string, metadata: number): Readonly | undefined { + public getGlyph(rasterizer: IGlyphRasterizer, chars: string, tokenMetadata: number, charMetadata: number): Readonly | undefined { // IMPORTANT: There are intentionally no intermediate variables here to aid in runtime // optimization as it's a very hot function - return this._glyphMap.get(chars, metadata, rasterizer.cacheKey) ?? this._createGlyph(rasterizer, chars, metadata); + return this._glyphMap.get(chars, tokenMetadata, charMetadata, rasterizer.cacheKey) ?? this._createGlyph(rasterizer, chars, tokenMetadata, charMetadata); } - private _createGlyph(rasterizer: IGlyphRasterizer, chars: string, metadata: number): Readonly | undefined { + private _createGlyph(rasterizer: IGlyphRasterizer, chars: string, tokenMetadata: number, charMetadata: number): Readonly | undefined { // Ensure the glyph can fit on the page if (this._glyphInOrderSet.size >= TextureAtlasPage.maximumGlyphCount) { return undefined; } // Rasterize and allocate the glyph - const rasterizedGlyph = rasterizer.rasterizeGlyph(chars, metadata, this._colorMap); + const rasterizedGlyph = rasterizer.rasterizeGlyph(chars, tokenMetadata, charMetadata, this._colorMap); const glyph = this._allocator.allocate(rasterizedGlyph); // Ensure the glyph was allocated @@ -89,7 +89,7 @@ export class TextureAtlasPage extends Disposable implements IReadableTextureAtla } // Save the glyph - this._glyphMap.set(chars, metadata, rasterizer.cacheKey, glyph); + this._glyphMap.set(chars, tokenMetadata, charMetadata, rasterizer.cacheKey, glyph); this._glyphInOrderSet.add(glyph); // Update page version and it's tracked used area @@ -100,7 +100,7 @@ export class TextureAtlasPage extends Disposable implements IReadableTextureAtla if (this._logService.getLevel() === LogLevel.Trace) { this._logService.trace('New glyph', { chars, - metadata, + metadata: tokenMetadata, rasterizedGlyph, glyph }); diff --git a/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts b/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts index d9f053c3a1a44..80bfb679b23a7 100644 --- a/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts +++ b/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts @@ -22,6 +22,7 @@ import { GPULifecycle } from './gpuDisposable.js'; import { quadVertices } from './gpuUtils.js'; import { GlyphRasterizer } from './raster/glyphRasterizer.js'; import { ViewGpuContext } from './viewGpuContext.js'; +import { GpuCharMetadata } from './raster/raster.js'; const enum Constants { @@ -333,7 +334,7 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend break; } chars = content.charAt(x); - charMetadata = tokenMetadata; + charMetadata = 0; // TODO: We'd want to optimize pulling the decorations in order // HACK: Temporary replace char to demonstrate inline decorations @@ -366,7 +367,7 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend } } - for (const [key, _value] of inlineStyles.entries()) { + for (const [key, value] of inlineStyles.entries()) { switch (key) { case 'text-decoration-line': { charMetadata |= MetadataConsts.STRIKETHROUGH_MASK; @@ -379,9 +380,18 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend break; } case 'color': { - // HACK: Set color requests to the first token's fg color - charMetadata &= ~MetadataConsts.FOREGROUND_MASK; - charMetadata |= 0b1 << MetadataConsts.FOREGROUND_OFFSET; + // TODO: Move to color.ts and make more generic + function parseRgb(text: string): number { + const color = text.match(/rgb\((\d+), (\d+), (\d+)\)/); + if (!color) { + throw new Error('Invalid color format'); + } + const r = parseInt(color[1], 10); + const g = parseInt(color[2], 10); + const b = parseInt(color[3], 10); + return r << 16 | g << 8 | b; + } + charMetadata = ((parseRgb(value) << GpuCharMetadata.FOREGROUND_OFFSET) & GpuCharMetadata.FOREGROUND_MASK) >>> 0; break; } default: throw new BugIndicatingError('Unexpected inline decoration style'); @@ -399,7 +409,7 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend continue; } - glyph = this._viewGpuContext.atlas.getGlyph(this._glyphRasterizer, chars, charMetadata); + glyph = this._viewGpuContext.atlas.getGlyph(this._glyphRasterizer, chars, tokenMetadata, charMetadata); // TODO: Support non-standard character widths absoluteOffsetX = Math.round((x + xOffset) * viewLineOptions.spaceWidth * dpr); diff --git a/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts b/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts index 07d343bb677c4..0c1c5db102e2c 100644 --- a/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts +++ b/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts @@ -9,7 +9,7 @@ import { Disposable } from '../../../../base/common/lifecycle.js'; import { StringBuilder } from '../../../common/core/stringBuilder.js'; import { FontStyle, TokenMetadata } from '../../../common/encodedTokenAttributes.js'; import { ensureNonNullable } from '../gpuUtils.js'; -import type { IBoundingBox, IGlyphRasterizer, IRasterizedGlyph } from './raster.js'; +import { GpuCharMetadata, type IBoundingBox, type IGlyphRasterizer, type IRasterizedGlyph } from './raster.js'; let nextId = 0; @@ -61,7 +61,8 @@ export class GlyphRasterizer extends Disposable implements IGlyphRasterizer { */ public rasterizeGlyph( chars: string, - metadata: number, + tokenMetadata: number, + charMetadata: number, colorMap: string[], ): Readonly { if (chars === '') { @@ -74,17 +75,18 @@ export class GlyphRasterizer extends Disposable implements IGlyphRasterizer { // Check if the last glyph matches the config, reuse if so. This helps avoid unnecessary // work when the rasterizer is called multiple times like when the glyph doesn't fit into a // page. - if (this._workGlyphConfig.chars === chars && this._workGlyphConfig.metadata === metadata) { + if (this._workGlyphConfig.chars === chars && this._workGlyphConfig.metadata === tokenMetadata) { return this._workGlyph; } this._workGlyphConfig.chars = chars; - this._workGlyphConfig.metadata = metadata; - return this._rasterizeGlyph(chars, metadata, colorMap); + this._workGlyphConfig.metadata = tokenMetadata; + return this._rasterizeGlyph(chars, tokenMetadata, charMetadata, colorMap); } public _rasterizeGlyph( chars: string, metadata: number, + charMetadata: number, colorMap: string[], ): Readonly { const devicePixelFontSize = Math.ceil(this._fontSize * getActiveWindow().devicePixelRatio); @@ -114,7 +116,12 @@ export class GlyphRasterizer extends Disposable implements IGlyphRasterizer { const originX = devicePixelFontSize; const originY = devicePixelFontSize; - this._ctx.fillStyle = colorMap[TokenMetadata.getForeground(metadata)]; + if (charMetadata) { + const fg = (charMetadata & GpuCharMetadata.FOREGROUND_MASK) >> GpuCharMetadata.FOREGROUND_OFFSET; + this._ctx.fillStyle = `#${fg.toString(16).padStart(6, '0')}`; + } else { + this._ctx.fillStyle = colorMap[TokenMetadata.getForeground(metadata)]; + } // TODO: This might actually be slower // const textMetrics = this._ctx.measureText(chars); this._ctx.textBaseline = 'top'; diff --git a/src/vs/editor/browser/gpu/raster/raster.ts b/src/vs/editor/browser/gpu/raster/raster.ts index 6eb41e680b299..d83b55665dbf5 100644 --- a/src/vs/editor/browser/gpu/raster/raster.ts +++ b/src/vs/editor/browser/gpu/raster/raster.ts @@ -21,13 +21,15 @@ export interface IGlyphRasterizer { * Rasterizes a glyph. * @param chars The character(s) to rasterize. This can be a single character, a ligature, an * emoji, etc. - * @param metadata The metadata of the glyph to rasterize. See {@link MetadataConsts} for how - * this works. + * @param tokenMetadata The token metadata of the glyph to rasterize. See {@link MetadataConsts} + * for how this works. + * @param charMetadata The chracter metadata of the glyph to rasterize. * @param colorMap A theme's color map. */ rasterizeGlyph( chars: string, - metadata: number, + tokenMetadata: number, + charMetadata: number, colorMap: string[], ): Readonly; } @@ -63,3 +65,11 @@ export interface IRasterizedGlyph { */ originOffset: { x: number; y: number }; } + +export const enum GpuCharMetadata { + FOREGROUND_MASK /* */ = 0b00000000_11111111_11111111_11111111, + OPACITY_MASK /* */ = 0b11111111_00000000_00000000_00000000, + + FOREGROUND_OFFSET = 0, + OPACITY_OFFSET = 24, +} diff --git a/src/vs/editor/browser/gpu/viewGpuContext.ts b/src/vs/editor/browser/gpu/viewGpuContext.ts index 162139b3cb82b..fdd4aa9f5efcf 100644 --- a/src/vs/editor/browser/gpu/viewGpuContext.ts +++ b/src/vs/editor/browser/gpu/viewGpuContext.ts @@ -150,6 +150,7 @@ export class ViewGpuContext extends Disposable { const styleRules = ViewGpuContext._decorationCssRuleExtractor.getStyleRules(container, decoration.inlineClassName); supported &&= styleRules.every(rule => { for (const r of rule.style) { + // TODO: Consider pseudo classes when checking for support if (!gpuSupportedCssRules.includes(r)) { return false; } From d3c1d2e831afd9ab4636bb0ee2cebe9fd60bf990 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 20 Nov 2024 09:55:57 -0800 Subject: [PATCH 008/119] Basic handling of alpha channel, ignore for now --- src/vs/editor/browser/gpu/fullFileRenderStrategy.ts | 7 ++----- src/vs/editor/contrib/gpu/browser/gpuActions.ts | 7 ++++--- .../test/browser/view/gpu/atlas/textureAtlas.test.ts | 7 ++++--- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts b/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts index 80bfb679b23a7..e3de4735d31fc 100644 --- a/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts +++ b/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts @@ -308,9 +308,6 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend e.range.startLineNumber <= y && e.range.endLineNumber >= y && e.options.inlineClassName )); - if (inlineDecorations.length > 0) { - console.log('decoration!', inlineDecorations); - } lineData = viewportData.getViewLineRenderingData(y); content = lineData.content; @@ -382,9 +379,9 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend case 'color': { // TODO: Move to color.ts and make more generic function parseRgb(text: string): number { - const color = text.match(/rgb\((\d+), (\d+), (\d+)\)/); + const color = text.match(/rgba?\((\d+), ?(\d+), ?(\d+)(?:, ?\d+(?:.\d+)?)?\)/); if (!color) { - throw new Error('Invalid color format'); + throw new Error('Invalid color format ' + text); } const r = parseInt(color[1], 10); const g = parseInt(color[2], 10); diff --git a/src/vs/editor/contrib/gpu/browser/gpuActions.ts b/src/vs/editor/contrib/gpu/browser/gpuActions.ts index 29de263652703..f64ac1c738047 100644 --- a/src/vs/editor/contrib/gpu/browser/gpuActions.ts +++ b/src/vs/editor/contrib/gpu/browser/gpuActions.ts @@ -115,8 +115,9 @@ class DebugEditorGpuRendererAction extends EditorAction { if (codePoint !== undefined) { chars = String.fromCodePoint(parseInt(codePoint, 16)); } - const metadata = 0; - const rasterizedGlyph = atlas.getGlyph(rasterizer, chars, metadata); + const tokenMetadata = 0; + const charMetadata = 0; + const rasterizedGlyph = atlas.getGlyph(rasterizer, chars, tokenMetadata, charMetadata); if (!rasterizedGlyph) { return; } @@ -133,7 +134,7 @@ class DebugEditorGpuRendererAction extends EditorAction { const ctx = ensureNonNullable(canvas.getContext('2d')); ctx.putImageData(imageData, 0, 0); const blob = await canvas.convertToBlob({ type: 'image/png' }); - const resource = URI.joinPath(folders[0].uri, `glyph_${chars}_${metadata}_${fontSize}px_${fontFamily.replaceAll(/[,\\\/\.'\s]/g, '_')}.png`); + const resource = URI.joinPath(folders[0].uri, `glyph_${chars}_${tokenMetadata}_${fontSize}px_${fontFamily.replaceAll(/[,\\\/\.'\s]/g, '_')}.png`); await fileService.writeFile(resource, VSBuffer.wrap(new Uint8Array(await blob.arrayBuffer()))); }); break; diff --git a/src/vs/editor/test/browser/view/gpu/atlas/textureAtlas.test.ts b/src/vs/editor/test/browser/view/gpu/atlas/textureAtlas.test.ts index 5a4353a1d19b4..3445beee97794 100644 --- a/src/vs/editor/test/browser/view/gpu/atlas/textureAtlas.test.ts +++ b/src/vs/editor/test/browser/view/gpu/atlas/textureAtlas.test.ts @@ -14,15 +14,16 @@ import { assertIsValidGlyph } from './testUtil.js'; import { TextureAtlasSlabAllocator } from '../../../../../browser/gpu/atlas/textureAtlasSlabAllocator.js'; const blackInt = 0x000000FF; +const nullCharMetadata = 0x0; let lastUniqueGlyph: string | undefined; -function getUniqueGlyphId(): [chars: string, tokenFg: number] { +function getUniqueGlyphId(): [chars: string, tokenMetadata: number, charMetadata: number] { if (!lastUniqueGlyph) { lastUniqueGlyph = 'a'; } else { lastUniqueGlyph = String.fromCharCode(lastUniqueGlyph.charCodeAt(0) + 1); } - return [lastUniqueGlyph, blackInt]; + return [lastUniqueGlyph, blackInt, nullCharMetadata]; } class TestGlyphRasterizer implements IGlyphRasterizer { @@ -30,7 +31,7 @@ class TestGlyphRasterizer implements IGlyphRasterizer { readonly cacheKey = ''; nextGlyphColor: [number, number, number, number] = [0, 0, 0, 0]; nextGlyphDimensions: [number, number] = [0, 0]; - rasterizeGlyph(chars: string, metadata: number, colorMap: string[]): Readonly { + rasterizeGlyph(chars: string, tokenMetadata: number, charMetadata: number, colorMap: string[]): Readonly { const w = this.nextGlyphDimensions[0]; const h = this.nextGlyphDimensions[1]; if (w === 0 || h === 0) { From 37e6cb3300061f0b7481011e60a459e917b6dd5a Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 20 Nov 2024 10:09:52 -0800 Subject: [PATCH 009/119] Clean up DecorationCssRulerExtractor DOM nodes This was causing mouse event request paths to get messed up --- src/vs/editor/browser/gpu/decorationCssRuleExtractor.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/vs/editor/browser/gpu/decorationCssRuleExtractor.ts b/src/vs/editor/browser/gpu/decorationCssRuleExtractor.ts index 5959bd36cd3e6..d76761e4c1ec5 100644 --- a/src/vs/editor/browser/gpu/decorationCssRuleExtractor.ts +++ b/src/vs/editor/browser/gpu/decorationCssRuleExtractor.ts @@ -25,14 +25,18 @@ export class DecorationCssRuleExtractor extends Disposable { } const dummyElement = $(`span.${decorationClassName}`); this._container.appendChild(dummyElement); + canvas.appendChild(this._container); + const rules = this._getStyleRules(canvas, dummyElement, decorationClassName); this._ruleCache.set(decorationClassName, rules); + + canvas.removeChild(this._container); + this._container.removeChild(dummyElement); + return rules; } private _getStyleRules(canvas: HTMLElement, element: HTMLElement, className: string) { - canvas.appendChild(this._container); - // Iterate through all stylesheets and imported stylesheets to find matching rules const rules = []; const doc = getActiveDocument(); From 1f900684f6304d1b1e50d8f7f6d2113a29aa152d Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 20 Nov 2024 10:22:48 -0800 Subject: [PATCH 010/119] Move CSS out and improve lifecycle of DecorationCssRuleExtractor --- .../browser/gpu/decorationCssRuleExtractor.ts | 28 +++++++++++++------ .../gpu/media/decorationCssRuleExtractor.css | 9 ++++++ 2 files changed, 29 insertions(+), 8 deletions(-) create mode 100644 src/vs/editor/browser/gpu/media/decorationCssRuleExtractor.css diff --git a/src/vs/editor/browser/gpu/decorationCssRuleExtractor.ts b/src/vs/editor/browser/gpu/decorationCssRuleExtractor.ts index d76761e4c1ec5..27b52a3e54381 100644 --- a/src/vs/editor/browser/gpu/decorationCssRuleExtractor.ts +++ b/src/vs/editor/browser/gpu/decorationCssRuleExtractor.ts @@ -5,38 +5,50 @@ import { $, getActiveDocument } from '../../../base/browser/dom.js'; import { Disposable, toDisposable } from '../../../base/common/lifecycle.js'; +import './media/decorationCssRuleExtractor.css'; +/** + * Extracts CSS rules that would be applied to certain decoration classes. + */ export class DecorationCssRuleExtractor extends Disposable { private _container: HTMLElement; + private _dummyElement: HTMLSpanElement; private _ruleCache: Map = new Map(); constructor() { super(); - this._container = $('div.monaco-css-rule-extractor'); - this._container.style.visibility = 'hidden'; + + this._container = $('div.monaco-decoration-css-rule-extractor'); + this._dummyElement = $('span'); + this._container.appendChild(this._dummyElement); + this._register(toDisposable(() => this._container.remove())); } getStyleRules(canvas: HTMLElement, decorationClassName: string): CSSStyleRule[] { + // Check cache const existing = this._ruleCache.get(decorationClassName); if (existing) { return existing; } - const dummyElement = $(`span.${decorationClassName}`); - this._container.appendChild(dummyElement); + + // Set up DOM + this._dummyElement.classList.add(decorationClassName); canvas.appendChild(this._container); - const rules = this._getStyleRules(canvas, dummyElement, decorationClassName); + // Get rules + const rules = this._getStyleRules(decorationClassName); this._ruleCache.set(decorationClassName, rules); + // Tear down DOM canvas.removeChild(this._container); - this._container.removeChild(dummyElement); + this._dummyElement.classList.remove(decorationClassName); return rules; } - private _getStyleRules(canvas: HTMLElement, element: HTMLElement, className: string) { + private _getStyleRules(className: string) { // Iterate through all stylesheets and imported stylesheets to find matching rules const rules = []; const doc = getActiveDocument(); @@ -49,7 +61,7 @@ export class DecorationCssRuleExtractor extends Disposable { stylesheets.push(rule.styleSheet); } } else if (rule instanceof CSSStyleRule) { - if (element.matches(rule.selectorText) && rule.selectorText.includes(`.${className}`)) { + if (this._dummyElement.matches(rule.selectorText) && rule.selectorText.includes(`.${className}`)) { rules.push(rule); } } diff --git a/src/vs/editor/browser/gpu/media/decorationCssRuleExtractor.css b/src/vs/editor/browser/gpu/media/decorationCssRuleExtractor.css new file mode 100644 index 0000000000000..900154c64fdf8 --- /dev/null +++ b/src/vs/editor/browser/gpu/media/decorationCssRuleExtractor.css @@ -0,0 +1,9 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.monaco-editor .monaco-decoration-css-rule-extractor { + visibility: hidden; + pointer-events: none; +} From 3489890a810a8aee40ac77c31dc913b30a64e2ed Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 20 Nov 2024 10:24:57 -0800 Subject: [PATCH 011/119] Move gpu/ test folder into correct new place --- .../test/browser/{view => }/gpu/atlas/testUtil.ts | 8 ++++---- .../{view => }/gpu/atlas/textureAtlas.test.ts | 14 +++++++------- .../gpu/atlas/textureAtlasAllocator.test.ts | 14 +++++++------- .../{view => }/gpu/bufferDirtyTracker.test.ts | 4 ++-- .../{view => }/gpu/objectCollectionBuffer.test.ts | 4 ++-- 5 files changed, 22 insertions(+), 22 deletions(-) rename src/vs/editor/test/browser/{view => }/gpu/atlas/testUtil.ts (86%) rename src/vs/editor/test/browser/{view => }/gpu/atlas/textureAtlas.test.ts (90%) rename src/vs/editor/test/browser/{view => }/gpu/atlas/textureAtlasAllocator.test.ts (92%) rename src/vs/editor/test/browser/{view => }/gpu/bufferDirtyTracker.test.ts (91%) rename src/vs/editor/test/browser/{view => }/gpu/objectCollectionBuffer.test.ts (97%) diff --git a/src/vs/editor/test/browser/view/gpu/atlas/testUtil.ts b/src/vs/editor/test/browser/gpu/atlas/testUtil.ts similarity index 86% rename from src/vs/editor/test/browser/view/gpu/atlas/testUtil.ts rename to src/vs/editor/test/browser/gpu/atlas/testUtil.ts index 73d1e167f1e15..ddc3f1583a064 100644 --- a/src/vs/editor/test/browser/view/gpu/atlas/testUtil.ts +++ b/src/vs/editor/test/browser/gpu/atlas/testUtil.ts @@ -4,10 +4,10 @@ *--------------------------------------------------------------------------------------------*/ import { fail, ok } from 'assert'; -import type { ITextureAtlasPageGlyph } from '../../../../../browser/gpu/atlas/atlas.js'; -import { TextureAtlas } from '../../../../../browser/gpu/atlas/textureAtlas.js'; -import { isNumber } from '../../../../../../base/common/types.js'; -import { ensureNonNullable } from '../../../../../browser/gpu/gpuUtils.js'; +import type { ITextureAtlasPageGlyph } from '../../../../browser/gpu/atlas/atlas.js'; +import { TextureAtlas } from '../../../../browser/gpu/atlas/textureAtlas.js'; +import { isNumber } from '../../../../../base/common/types.js'; +import { ensureNonNullable } from '../../../../browser/gpu/gpuUtils.js'; export function assertIsValidGlyph(glyph: Readonly | undefined, atlasOrSource: TextureAtlas | OffscreenCanvas) { if (glyph === undefined) { diff --git a/src/vs/editor/test/browser/view/gpu/atlas/textureAtlas.test.ts b/src/vs/editor/test/browser/gpu/atlas/textureAtlas.test.ts similarity index 90% rename from src/vs/editor/test/browser/view/gpu/atlas/textureAtlas.test.ts rename to src/vs/editor/test/browser/gpu/atlas/textureAtlas.test.ts index 3445beee97794..0610f84eb3e5e 100644 --- a/src/vs/editor/test/browser/view/gpu/atlas/textureAtlas.test.ts +++ b/src/vs/editor/test/browser/gpu/atlas/textureAtlas.test.ts @@ -4,14 +4,14 @@ *--------------------------------------------------------------------------------------------*/ import { strictEqual, throws } from 'assert'; -import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; -import type { IGlyphRasterizer, IRasterizedGlyph } from '../../../../../browser/gpu/raster/raster.js'; -import { ensureNonNullable } from '../../../../../browser/gpu/gpuUtils.js'; -import type { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; -import { TextureAtlas } from '../../../../../browser/gpu/atlas/textureAtlas.js'; -import { createCodeEditorServices } from '../../../testCodeEditor.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; +import type { IGlyphRasterizer, IRasterizedGlyph } from '../../../../browser/gpu/raster/raster.js'; +import { ensureNonNullable } from '../../../../browser/gpu/gpuUtils.js'; +import type { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; +import { TextureAtlas } from '../../../../browser/gpu/atlas/textureAtlas.js'; +import { createCodeEditorServices } from '../../testCodeEditor.js'; import { assertIsValidGlyph } from './testUtil.js'; -import { TextureAtlasSlabAllocator } from '../../../../../browser/gpu/atlas/textureAtlasSlabAllocator.js'; +import { TextureAtlasSlabAllocator } from '../../../../browser/gpu/atlas/textureAtlasSlabAllocator.js'; const blackInt = 0x000000FF; const nullCharMetadata = 0x0; diff --git a/src/vs/editor/test/browser/view/gpu/atlas/textureAtlasAllocator.test.ts b/src/vs/editor/test/browser/gpu/atlas/textureAtlasAllocator.test.ts similarity index 92% rename from src/vs/editor/test/browser/view/gpu/atlas/textureAtlasAllocator.test.ts rename to src/vs/editor/test/browser/gpu/atlas/textureAtlasAllocator.test.ts index 92d354a5f789d..377bb752df8a0 100644 --- a/src/vs/editor/test/browser/view/gpu/atlas/textureAtlasAllocator.test.ts +++ b/src/vs/editor/test/browser/gpu/atlas/textureAtlasAllocator.test.ts @@ -4,14 +4,14 @@ *--------------------------------------------------------------------------------------------*/ import { deepStrictEqual, strictEqual, throws } from 'assert'; -import type { IRasterizedGlyph } from '../../../../../browser/gpu/raster/raster.js'; -import { ensureNonNullable } from '../../../../../browser/gpu/gpuUtils.js'; -import type { ITextureAtlasAllocator } from '../../../../../browser/gpu/atlas/atlas.js'; -import { TextureAtlasShelfAllocator } from '../../../../../browser/gpu/atlas/textureAtlasShelfAllocator.js'; -import { TextureAtlasSlabAllocator, type TextureAtlasSlabAllocatorOptions } from '../../../../../browser/gpu/atlas/textureAtlasSlabAllocator.js'; -import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; +import type { IRasterizedGlyph } from '../../../../browser/gpu/raster/raster.js'; +import { ensureNonNullable } from '../../../../browser/gpu/gpuUtils.js'; +import type { ITextureAtlasAllocator } from '../../../../browser/gpu/atlas/atlas.js'; +import { TextureAtlasShelfAllocator } from '../../../../browser/gpu/atlas/textureAtlasShelfAllocator.js'; +import { TextureAtlasSlabAllocator, type TextureAtlasSlabAllocatorOptions } from '../../../../browser/gpu/atlas/textureAtlasSlabAllocator.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; import { assertIsValidGlyph } from './testUtil.js'; -import { BugIndicatingError } from '../../../../../../base/common/errors.js'; +import { BugIndicatingError } from '../../../../../base/common/errors.js'; const blackArr = [0x00, 0x00, 0x00, 0xFF]; diff --git a/src/vs/editor/test/browser/view/gpu/bufferDirtyTracker.test.ts b/src/vs/editor/test/browser/gpu/bufferDirtyTracker.test.ts similarity index 91% rename from src/vs/editor/test/browser/view/gpu/bufferDirtyTracker.test.ts rename to src/vs/editor/test/browser/gpu/bufferDirtyTracker.test.ts index 0ddc7a5befeff..0794961644e0a 100644 --- a/src/vs/editor/test/browser/view/gpu/bufferDirtyTracker.test.ts +++ b/src/vs/editor/test/browser/gpu/bufferDirtyTracker.test.ts @@ -4,8 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import { strictEqual } from 'assert'; -import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; -import { BufferDirtyTracker } from '../../../../browser/gpu/bufferDirtyTracker.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; +import { BufferDirtyTracker } from '../../../browser/gpu/bufferDirtyTracker.js'; suite('BufferDirtyTracker', () => { ensureNoDisposablesAreLeakedInTestSuite(); diff --git a/src/vs/editor/test/browser/view/gpu/objectCollectionBuffer.test.ts b/src/vs/editor/test/browser/gpu/objectCollectionBuffer.test.ts similarity index 97% rename from src/vs/editor/test/browser/view/gpu/objectCollectionBuffer.test.ts rename to src/vs/editor/test/browser/gpu/objectCollectionBuffer.test.ts index ae8c0670d2b8b..52ca0b7619f39 100644 --- a/src/vs/editor/test/browser/view/gpu/objectCollectionBuffer.test.ts +++ b/src/vs/editor/test/browser/gpu/objectCollectionBuffer.test.ts @@ -4,8 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import { deepStrictEqual, strictEqual } from 'assert'; -import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; -import { createObjectCollectionBuffer, type IObjectCollectionBuffer } from '../../../../browser/gpu/objectCollectionBuffer.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; +import { createObjectCollectionBuffer, type IObjectCollectionBuffer } from '../../../browser/gpu/objectCollectionBuffer.js'; suite('ObjectCollectionBuffer', () => { const store = ensureNoDisposablesAreLeakedInTestSuite(); From 01e1c7c01fb267d5b0a95688dd6b092afe839fcf Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 20 Nov 2024 10:46:08 -0800 Subject: [PATCH 012/119] Add unit tests for DecorationCssRuleExtractor --- .../browser/gpu/decorationCssRuleExtractor.ts | 5 +- .../gpu/decorationCssRulerExtractor.test.ts | 83 +++++++++++++++++++ 2 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 src/vs/editor/test/browser/gpu/decorationCssRulerExtractor.test.ts diff --git a/src/vs/editor/browser/gpu/decorationCssRuleExtractor.ts b/src/vs/editor/browser/gpu/decorationCssRuleExtractor.ts index 27b52a3e54381..c05f2d63418d4 100644 --- a/src/vs/editor/browser/gpu/decorationCssRuleExtractor.ts +++ b/src/vs/editor/browser/gpu/decorationCssRuleExtractor.ts @@ -61,7 +61,10 @@ export class DecorationCssRuleExtractor extends Disposable { stylesheets.push(rule.styleSheet); } } else if (rule instanceof CSSStyleRule) { - if (this._dummyElement.matches(rule.selectorText) && rule.selectorText.includes(`.${className}`)) { + // Note that originally `.matches(rule.selectorText)` was used but this would + // not pick up pseudo-classes which are important to determine support of the + // returned styles. + if (rule.selectorText.includes(`.${className}`)) { rules.push(rule); } } diff --git a/src/vs/editor/test/browser/gpu/decorationCssRulerExtractor.test.ts b/src/vs/editor/test/browser/gpu/decorationCssRulerExtractor.test.ts new file mode 100644 index 0000000000000..ddeffcdb82909 --- /dev/null +++ b/src/vs/editor/test/browser/gpu/decorationCssRulerExtractor.test.ts @@ -0,0 +1,83 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { deepStrictEqual } from 'assert'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; +import { DecorationCssRuleExtractor } from '../../../browser/gpu/decorationCssRuleExtractor.js'; +import { generateUuid } from '../../../../base/common/uuid.js'; +import { $, getActiveDocument } from '../../../../base/browser/dom.js'; + +function randomClass(): string { + return 'test-class-' + generateUuid(); +} + +suite('DecorationCssRulerExtractor', () => { + const store = ensureNoDisposablesAreLeakedInTestSuite(); + + let doc: Document; + let container: HTMLElement; + let extractor: DecorationCssRuleExtractor; + let testClassName: string; + + function addStyleElement(content: string): void { + const styleElement = $('style'); + styleElement.textContent = content; + container.append(styleElement); + } + + function assertStyles(className: string, expectedCssText: string[]): void { + deepStrictEqual(extractor.getStyleRules(container, className).map(e => e.cssText), expectedCssText); + } + + setup(() => { + doc = getActiveDocument(); + extractor = store.add(new DecorationCssRuleExtractor()); + testClassName = randomClass(); + container = $('div'); + doc.body.append(container); + }); + + teardown(() => { + container.remove(); + }); + + test('unknown class should give no styles', () => { + assertStyles(randomClass(), []); + }); + + test('single style should be picked up', () => { + addStyleElement(`.${testClassName} { color: red; }`); + assertStyles(testClassName, [ + `.${testClassName} { color: red; }` + ]); + }); + + test('multiple styles from the same selector should be picked up', () => { + addStyleElement(`.${testClassName} { color: red; opacity: 0.5; }`); + assertStyles(testClassName, [ + `.${testClassName} { color: red; opacity: 0.5; }` + ]); + }); + + test('multiple styles from different selectors should be picked up', () => { + addStyleElement([ + `.${testClassName} { color: red; opacity: 0.5; }`, + `.${testClassName}:hover { opacity: 1; }`, + ].join('\n')); + assertStyles(testClassName, [ + `.${testClassName} { color: red; opacity: 0.5; }`, + `.${testClassName}:hover { opacity: 1; }`, + ]); + }); + + test('multiple styles from the different stylesheets should be picked up', () => { + addStyleElement(`.${testClassName} { color: red; opacity: 0.5; }`); + addStyleElement(`.${testClassName}:hover { opacity: 1; }`); + assertStyles(testClassName, [ + `.${testClassName} { color: red; opacity: 0.5; }`, + `.${testClassName}:hover { opacity: 1; }`, + ]); + }); +}); From 46abc8b541003676268bf477e07576a50d0ad271 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 20 Nov 2024 10:54:40 -0800 Subject: [PATCH 013/119] Don't support lines with pseudo classes --- src/vs/editor/browser/gpu/viewGpuContext.ts | 29 ++++++++++++++------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/src/vs/editor/browser/gpu/viewGpuContext.ts b/src/vs/editor/browser/gpu/viewGpuContext.ts index 2da2c770ffca6..883c75d75bdd6 100644 --- a/src/vs/editor/browser/gpu/viewGpuContext.ts +++ b/src/vs/editor/browser/gpu/viewGpuContext.ts @@ -160,9 +160,12 @@ export class ViewGpuContext extends Disposable { for (const decoration of data.inlineDecorations) { const styleRules = ViewGpuContext._decorationCssRuleExtractor.getStyleRules(container, decoration.inlineClassName); supported &&= styleRules.every(rule => { + // Pseudo classes aren't supported currently + if (rule.selectorText.includes(':')) { + return false; + } for (const r of rule.style) { - // TODO: Consider pseudo classes when checking for support - if (!gpuSupportedCssRules.includes(r)) { + if (!gpuSupportedDecorationCssRules.includes(r)) { return false; } } @@ -179,7 +182,7 @@ export class ViewGpuContext extends Disposable { } /** - * Like {@link canRender} but returned detailed information about why the line cannot be rendered. + * Like {@link canRender} but returns detailed information about why the line cannot be rendered. */ public static canRenderDetailed(container: HTMLElement, options: ViewLineOptions, viewportData: ViewportData, lineNumber: number): string[] { const data = viewportData.getViewLineRenderingData(lineNumber); @@ -195,12 +198,18 @@ export class ViewGpuContext extends Disposable { } if (data.inlineDecorations.length > 0) { let supported = true; + const problemSelectors: string[] = []; const problemRules: string[] = []; for (const decoration of data.inlineDecorations) { const styleRules = ViewGpuContext._decorationCssRuleExtractor.getStyleRules(container, decoration.inlineClassName); supported &&= styleRules.every(rule => { + // Pseudo classes aren't supported currently + if (rule.selectorText.includes(':')) { + problemSelectors.push(rule.selectorText); + return false; + } for (const r of rule.style) { - if (!gpuSupportedCssRules.includes(r)) { + if (!gpuSupportedDecorationCssRules.includes(r)) { problemRules.push(r); return false; } @@ -214,6 +223,9 @@ export class ViewGpuContext extends Disposable { if (problemRules.length > 0) { reasons.push(`inlineDecorations with unsupported CSS rules (\`${problemRules.join(', ')}\`)`); } + if (problemSelectors.length > 0) { + reasons.push(`inlineDecorations with unsupported CSS selectors (\`${problemSelectors.join(', ')}\`)`); + } } if (lineNumber >= GpuRenderLimits.maxGpuLines) { reasons.push('lineNumber >= maxGpuLines'); @@ -222,10 +234,9 @@ export class ViewGpuContext extends Disposable { } } -const gpuSupportedCssRules = [ +/** + * A list of fully supported decoration CSS rules that can be used in the GPU renderer. + */ +const gpuSupportedDecorationCssRules = [ 'color', - // 'text-decoration-line', - // 'text-decoration-thickness', - // 'text-decoration-style', - // 'text-decoration-color', ]; From f87274f213dec5bd4aaf5872b2302017ae6ca7bd Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 20 Nov 2024 11:48:55 -0800 Subject: [PATCH 014/119] Move general parse function to color.ts and add some tests --- src/vs/base/common/color.ts | 37 +++++ src/vs/base/test/common/color.test.ts | 136 ++++++++++++++++++ .../browser/gpu/fullFileRenderStrategy.ts | 22 +-- 3 files changed, 184 insertions(+), 11 deletions(-) diff --git a/src/vs/base/common/color.ts b/src/vs/base/common/color.ts index caa1d4419f007..a4faedd349956 100644 --- a/src/vs/base/common/color.ts +++ b/src/vs/base/common/color.ts @@ -630,6 +630,43 @@ export namespace Color { return Color.Format.CSS.formatRGBA(color); } + /** + * Parse a CSS color and return a {@link Color}. + * @param css The CSS color to parse. + * @see https://drafts.csswg.org/css-color/#typedef-color + */ + export function parse(css: string): Color | null { + if (css === 'transparent') { + return Color.transparent; + } + if (css.startsWith('#')) { + return parseHex(css); + } + if (css.startsWith('rgba(')) { + const color = css.match(/rgba\((?(?:\+|-)?\d+), *(?(?:\+|-)?\d+), *(?(?:\+|-)?\d+), *(?(?:\+|-)?\d+(\.\d+)?)\)/); + if (!color) { + throw new Error('Invalid color format ' + css); + } + const r = parseInt(color.groups?.r ?? '0'); + const g = parseInt(color.groups?.g ?? '0'); + const b = parseInt(color.groups?.b ?? '0'); + const a = parseFloat(color.groups?.a ?? '0'); + return new Color(new RGBA(r, g, b, a)); + } + if (css.startsWith('rgb(')) { + const color = css.match(/rgb\((?(?:\+|-)?\d+), *(?(?:\+|-)?\d+), *(?(?:\+|-)?\d+)\)/); + if (!color) { + throw new Error('Invalid color format ' + css); + } + const r = parseInt(color.groups?.r ?? '0'); + const g = parseInt(color.groups?.g ?? '0'); + const b = parseInt(color.groups?.b ?? '0'); + return new Color(new RGBA(r, g, b)); + } + // TODO: Support more formats + return null; + } + /** * Converts an Hex color value to a Color. * returns r, g, and b are contained in the set [0, 255] diff --git a/src/vs/base/test/common/color.test.ts b/src/vs/base/test/common/color.test.ts index 0f6c1a689cf84..3ae8353bcdff3 100644 --- a/src/vs/base/test/common/color.test.ts +++ b/src/vs/base/test/common/color.test.ts @@ -204,6 +204,142 @@ suite('Color', () => { suite('Format', () => { suite('CSS', () => { + suite('parse', () => { + test('invalid', () => { + assert.deepStrictEqual(Color.Format.CSS.parse(''), null); + assert.deepStrictEqual(Color.Format.CSS.parse('#'), null); + assert.deepStrictEqual(Color.Format.CSS.parse('#0102030'), null); + }); + test('transparent', () => { + assert.deepStrictEqual(Color.Format.CSS.parse('transparent'), new Color(new RGBA(0, 0, 0, 0))); + }); + test('hex-color', () => { + // somewhat valid + assert.deepStrictEqual(Color.Format.CSS.parse('#FFFFG0')!.rgba, new RGBA(255, 255, 0, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('#FFFFg0')!.rgba, new RGBA(255, 255, 0, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('#-FFF00')!.rgba, new RGBA(15, 255, 0, 1)); + + // valid + assert.deepStrictEqual(Color.Format.CSS.parse('#000000')!.rgba, new RGBA(0, 0, 0, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('#FFFFFF')!.rgba, new RGBA(255, 255, 255, 1)); + + assert.deepStrictEqual(Color.Format.CSS.parse('#FF0000')!.rgba, new RGBA(255, 0, 0, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('#00FF00')!.rgba, new RGBA(0, 255, 0, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('#0000FF')!.rgba, new RGBA(0, 0, 255, 1)); + + assert.deepStrictEqual(Color.Format.CSS.parse('#FFFF00')!.rgba, new RGBA(255, 255, 0, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('#00FFFF')!.rgba, new RGBA(0, 255, 255, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('#FF00FF')!.rgba, new RGBA(255, 0, 255, 1)); + + assert.deepStrictEqual(Color.Format.CSS.parse('#C0C0C0')!.rgba, new RGBA(192, 192, 192, 1)); + + assert.deepStrictEqual(Color.Format.CSS.parse('#808080')!.rgba, new RGBA(128, 128, 128, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('#800000')!.rgba, new RGBA(128, 0, 0, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('#808000')!.rgba, new RGBA(128, 128, 0, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('#008000')!.rgba, new RGBA(0, 128, 0, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('#800080')!.rgba, new RGBA(128, 0, 128, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('#008080')!.rgba, new RGBA(0, 128, 128, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('#000080')!.rgba, new RGBA(0, 0, 128, 1)); + + assert.deepStrictEqual(Color.Format.CSS.parse('#010203')!.rgba, new RGBA(1, 2, 3, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('#040506')!.rgba, new RGBA(4, 5, 6, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('#070809')!.rgba, new RGBA(7, 8, 9, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('#0a0A0a')!.rgba, new RGBA(10, 10, 10, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('#0b0B0b')!.rgba, new RGBA(11, 11, 11, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('#0c0C0c')!.rgba, new RGBA(12, 12, 12, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('#0d0D0d')!.rgba, new RGBA(13, 13, 13, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('#0e0E0e')!.rgba, new RGBA(14, 14, 14, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('#0f0F0f')!.rgba, new RGBA(15, 15, 15, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('#a0A0a0')!.rgba, new RGBA(160, 160, 160, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('#CFA')!.rgba, new RGBA(204, 255, 170, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('#CFA8')!.rgba, new RGBA(204, 255, 170, 0.533)); + }); + + test('rgb()', () => { + // somewhat valid / unusual + assert.deepStrictEqual(Color.Format.CSS.parse('rgb(-255, 0, 0)')!.rgba, new RGBA(0, 0, 0)); + assert.deepStrictEqual(Color.Format.CSS.parse('rgb(+255, 0, 0)')!.rgba, new RGBA(255, 0, 0)); + assert.deepStrictEqual(Color.Format.CSS.parse('rgb(800, 0, 0)')!.rgba, new RGBA(255, 0, 0)); + + // valid + assert.deepStrictEqual(Color.Format.CSS.parse('rgb(0, 0, 0)')!.rgba, new RGBA(0, 0, 0, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('rgb(255, 255, 255)')!.rgba, new RGBA(255, 255, 255, 1)); + + assert.deepStrictEqual(Color.Format.CSS.parse('rgb(255, 0, 0)')!.rgba, new RGBA(255, 0, 0, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('rgb(0, 255, 0)')!.rgba, new RGBA(0, 255, 0, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('rgb(0, 0, 255)')!.rgba, new RGBA(0, 0, 255, 1)); + + assert.deepStrictEqual(Color.Format.CSS.parse('rgb(255, 255, 0)')!.rgba, new RGBA(255, 255, 0, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('rgb(0, 255, 255)')!.rgba, new RGBA(0, 255, 255, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('rgb(255, 0, 255)')!.rgba, new RGBA(255, 0, 255, 1)); + + assert.deepStrictEqual(Color.Format.CSS.parse('rgb(192, 192, 192)')!.rgba, new RGBA(192, 192, 192, 1)); + + assert.deepStrictEqual(Color.Format.CSS.parse('rgb(128, 128, 128)')!.rgba, new RGBA(128, 128, 128, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('rgb(128, 0, 0)')!.rgba, new RGBA(128, 0, 0, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('rgb(128, 128, 0)')!.rgba, new RGBA(128, 128, 0, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('rgb(0, 128, 0)')!.rgba, new RGBA(0, 128, 0, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('rgb(128, 0, 128)')!.rgba, new RGBA(128, 0, 128, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('rgb(0, 128, 128)')!.rgba, new RGBA(0, 128, 128, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('rgb(0, 0, 128)')!.rgba, new RGBA(0, 0, 128, 1)); + + assert.deepStrictEqual(Color.Format.CSS.parse('rgb(1, 2, 3)')!.rgba, new RGBA(1, 2, 3, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('rgb(4, 5, 6)')!.rgba, new RGBA(4, 5, 6, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('rgb(7, 8, 9)')!.rgba, new RGBA(7, 8, 9, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('rgb(10, 10, 10)')!.rgba, new RGBA(10, 10, 10, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('rgb(11, 11, 11)')!.rgba, new RGBA(11, 11, 11, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('rgb(12, 12, 12)')!.rgba, new RGBA(12, 12, 12, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('rgb(13, 13, 13)')!.rgba, new RGBA(13, 13, 13, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('rgb(14, 14, 14)')!.rgba, new RGBA(14, 14, 14, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('rgb(15, 15, 15)')!.rgba, new RGBA(15, 15, 15, 1)); + }); + + test('rgba()', () => { + // somewhat valid / unusual + assert.deepStrictEqual(Color.Format.CSS.parse('rgba(0, 0, 0, 255)')!.rgba, new RGBA(0, 0, 0, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('rgba(-255, 0, 0, 1)')!.rgba, new RGBA(0, 0, 0, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('rgba(+255, 0, 0, 1)')!.rgba, new RGBA(255, 0, 0, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('rgba(800, 0, 0, 1)')!.rgba, new RGBA(255, 0, 0, 1)); + + // alpha values + assert.deepStrictEqual(Color.Format.CSS.parse('rgba(255, 0, 0, 0.2)')!.rgba, new RGBA(255, 0, 0, 0.2)); + assert.deepStrictEqual(Color.Format.CSS.parse('rgba(255, 0, 0, 0.5)')!.rgba, new RGBA(255, 0, 0, 0.5)); + assert.deepStrictEqual(Color.Format.CSS.parse('rgba(255, 0, 0, 0.75)')!.rgba, new RGBA(255, 0, 0, 0.75)); + + // valid + assert.deepStrictEqual(Color.Format.CSS.parse('rgba(0, 0, 0, 1)')!.rgba, new RGBA(0, 0, 0, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('rgba(255, 255, 255, 1)')!.rgba, new RGBA(255, 255, 255, 1)); + + assert.deepStrictEqual(Color.Format.CSS.parse('rgba(255, 0, 0, 1)')!.rgba, new RGBA(255, 0, 0, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('rgba(0, 255, 0, 1)')!.rgba, new RGBA(0, 255, 0, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('rgba(0, 0, 255, 1)')!.rgba, new RGBA(0, 0, 255, 1)); + + assert.deepStrictEqual(Color.Format.CSS.parse('rgba(255, 255, 0, 1)')!.rgba, new RGBA(255, 255, 0, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('rgba(0, 255, 255, 1)')!.rgba, new RGBA(0, 255, 255, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('rgba(255, 0, 255, 1)')!.rgba, new RGBA(255, 0, 255, 1)); + + assert.deepStrictEqual(Color.Format.CSS.parse('rgba(192, 192, 192, 1)')!.rgba, new RGBA(192, 192, 192, 1)); + + assert.deepStrictEqual(Color.Format.CSS.parse('rgba(128, 128, 128, 1)')!.rgba, new RGBA(128, 128, 128, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('rgba(128, 0, 0, 1)')!.rgba, new RGBA(128, 0, 0, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('rgba(128, 128, 0, 1)')!.rgba, new RGBA(128, 128, 0, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('rgba(0, 128, 0, 1)')!.rgba, new RGBA(0, 128, 0, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('rgba(128, 0, 128, 1)')!.rgba, new RGBA(128, 0, 128, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('rgba(0, 128, 128, 1)')!.rgba, new RGBA(0, 128, 128, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('rgba(0, 0, 128, 1)')!.rgba, new RGBA(0, 0, 128, 1)); + + assert.deepStrictEqual(Color.Format.CSS.parse('rgba(1, 2, 3, 1)')!.rgba, new RGBA(1, 2, 3, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('rgba(4, 5, 6, 1)')!.rgba, new RGBA(4, 5, 6, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('rgba(7, 8, 9, 1)')!.rgba, new RGBA(7, 8, 9, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('rgba(10, 10, 10, 1)')!.rgba, new RGBA(10, 10, 10, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('rgba(11, 11, 11, 1)')!.rgba, new RGBA(11, 11, 11, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('rgba(12, 12, 12, 1)')!.rgba, new RGBA(12, 12, 12, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('rgba(13, 13, 13, 1)')!.rgba, new RGBA(13, 13, 13, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('rgba(14, 14, 14, 1)')!.rgba, new RGBA(14, 14, 14, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('rgba(15, 15, 15, 1)')!.rgba, new RGBA(15, 15, 15, 1)); + }); + }); + test('parseHex', () => { // invalid diff --git a/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts b/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts index 941599a1143c8..b1c99760b28f5 100644 --- a/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts +++ b/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts @@ -24,6 +24,7 @@ import { quadVertices } from './gpuUtils.js'; import { GlyphRasterizer } from './raster/glyphRasterizer.js'; import { ViewGpuContext } from './viewGpuContext.js'; import { GpuCharMetadata } from './raster/raster.js'; +import { Color } from '../../../base/common/color.js'; const enum Constants { @@ -410,18 +411,17 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend break; } case 'color': { - // TODO: Move to color.ts and make more generic - function parseRgb(text: string): number { - const color = text.match(/rgba?\((\d+), ?(\d+), ?(\d+)(?:, ?\d+(?:.\d+)?)?\)/); - if (!color) { - throw new Error('Invalid color format ' + text); - } - const r = parseInt(color[1], 10); - const g = parseInt(color[2], 10); - const b = parseInt(color[3], 10); - return r << 16 | g << 8 | b; + // TODO: This parsing/error handling should move into canRender so fallback to DOM works + const parsedColor = Color.Format.CSS.parse(value); + if (!parsedColor) { + throw new Error('Invalid color format ' + value); + } + const rgb = parsedColor.rgba.r << 16 | parsedColor.rgba.g << 8 | parsedColor.rgba.b; + charMetadata |= ((rgb << GpuCharMetadata.FOREGROUND_OFFSET) & GpuCharMetadata.FOREGROUND_MASK) >>> 0; + // TODO: _foreground_ opacity should not be applied to regular opacity + if (parsedColor.rgba.a < 1) { + charMetadata |= ((parsedColor.rgba.a * 0xFF << GpuCharMetadata.OPACITY_OFFSET) & GpuCharMetadata.OPACITY_MASK) >>> 0; } - charMetadata = ((parseRgb(value) << GpuCharMetadata.FOREGROUND_OFFSET) & GpuCharMetadata.FOREGROUND_MASK) >>> 0; break; } default: throw new BugIndicatingError('Unexpected inline decoration style'); From 17cf79ee97f4ab55f8b565ca8453fed74f692785 Mon Sep 17 00:00:00 2001 From: Osvaldo Ortega Date: Wed, 20 Nov 2024 16:15:20 -0800 Subject: [PATCH 015/119] Rename to editor group --- src/vs/editor/common/config/editorOptions.ts | 8 ++++---- src/vs/editor/contrib/find/browser/findWidget.ts | 2 +- src/vs/monaco.d.ts | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index 409a94bbc9420..d48c1d0b78c5a 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -1658,7 +1658,7 @@ export interface IEditorFindOptions { /** * Controls how the find widget search history should be stored */ - findSearchHistory?: 'never' | 'workspace' | 'editor'; + findSearchHistory?: 'never' | 'workspace' | 'editorGroup'; } /** @@ -1726,12 +1726,12 @@ class EditorFind extends BaseEditorOption(input.findSearchHistory, this.defaultValue.findSearchHistory, ['never', 'workspace', 'editor']), + findSearchHistory: stringSet<'never' | 'workspace' | 'editorGroup'>(input.findSearchHistory, this.defaultValue.findSearchHistory, ['never', 'workspace', 'editorGroup']), }; } } diff --git a/src/vs/editor/contrib/find/browser/findWidget.ts b/src/vs/editor/contrib/find/browser/findWidget.ts index dcbd6037aba2b..b95c086a45008 100644 --- a/src/vs/editor/contrib/find/browser/findWidget.ts +++ b/src/vs/editor/contrib/find/browser/findWidget.ts @@ -189,7 +189,7 @@ export class FindWidget extends Widget implements IOverlayWidget, IVerticalSashL const findSearchHistoryConfig = this._codeEditor.getOption(EditorOption.find).findSearchHistory; this._findWidgetSearchHistory = new FindWidgetSearchHistory( this._storageService, - findSearchHistoryConfig === 'editor' ? this._codeEditor : undefined + findSearchHistoryConfig === 'editorGroup' ? this._codeEditor : undefined ); this._ctrlEnterReplaceAllWarningPrompted = !!storageService.getBoolean(ctrlEnterReplaceAllWarningPromptedKey, StorageScope.PROFILE); diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 77bb8ebe83373..15ce7789e45f8 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -4064,7 +4064,7 @@ declare namespace monaco.editor { /** * Controls how the find widget search history should be stored */ - findSearchHistory?: 'never' | 'workspace' | 'editor'; + findSearchHistory?: 'never' | 'workspace' | 'editorGroup'; } export type GoToLocationValues = 'peek' | 'gotoAndPeek' | 'goto'; From 07d84aae4a81d32cf66954a32564c680f7584d46 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 21 Nov 2024 07:03:04 -0800 Subject: [PATCH 016/119] Add CSS named color support to parse function --- src/vs/base/common/color.ts | 157 +++++++++++++++++++++++++- src/vs/base/test/common/color.test.ts | 153 +++++++++++++++++++++++++ 2 files changed, 309 insertions(+), 1 deletion(-) diff --git a/src/vs/base/common/color.ts b/src/vs/base/common/color.ts index a4faedd349956..02d98667fb71d 100644 --- a/src/vs/base/common/color.ts +++ b/src/vs/base/common/color.ts @@ -664,7 +664,162 @@ export namespace Color { return new Color(new RGBA(r, g, b)); } // TODO: Support more formats - return null; + return parseNamedKeyword(css); + } + + function parseNamedKeyword(css: string): Color | null { + // https://drafts.csswg.org/css-color/#named-colors + switch (css) { + case 'aliceblue': return new Color(new RGBA(240, 248, 255, 1)); + case 'antiquewhite': return new Color(new RGBA(250, 235, 215, 1)); + case 'aqua': return new Color(new RGBA(0, 255, 255, 1)); + case 'aquamarine': return new Color(new RGBA(127, 255, 212, 1)); + case 'azure': return new Color(new RGBA(240, 255, 255, 1)); + case 'beige': return new Color(new RGBA(245, 245, 220, 1)); + case 'bisque': return new Color(new RGBA(255, 228, 196, 1)); + case 'black': return new Color(new RGBA(0, 0, 0, 1)); + case 'blanchedalmond': return new Color(new RGBA(255, 235, 205, 1)); + case 'blue': return new Color(new RGBA(0, 0, 255, 1)); + case 'blueviolet': return new Color(new RGBA(138, 43, 226, 1)); + case 'brown': return new Color(new RGBA(165, 42, 42, 1)); + case 'burlywood': return new Color(new RGBA(222, 184, 135, 1)); + case 'cadetblue': return new Color(new RGBA(95, 158, 160, 1)); + case 'chartreuse': return new Color(new RGBA(127, 255, 0, 1)); + case 'chocolate': return new Color(new RGBA(210, 105, 30, 1)); + case 'coral': return new Color(new RGBA(255, 127, 80, 1)); + case 'cornflowerblue': return new Color(new RGBA(100, 149, 237, 1)); + case 'cornsilk': return new Color(new RGBA(255, 248, 220, 1)); + case 'crimson': return new Color(new RGBA(220, 20, 60, 1)); + case 'cyan': return new Color(new RGBA(0, 255, 255, 1)); + case 'darkblue': return new Color(new RGBA(0, 0, 139, 1)); + case 'darkcyan': return new Color(new RGBA(0, 139, 139, 1)); + case 'darkgoldenrod': return new Color(new RGBA(184, 134, 11, 1)); + case 'darkgray': return new Color(new RGBA(169, 169, 169, 1)); + case 'darkgreen': return new Color(new RGBA(0, 100, 0, 1)); + case 'darkgrey': return new Color(new RGBA(169, 169, 169, 1)); + case 'darkkhaki': return new Color(new RGBA(189, 183, 107, 1)); + case 'darkmagenta': return new Color(new RGBA(139, 0, 139, 1)); + case 'darkolivegreen': return new Color(new RGBA(85, 107, 47, 1)); + case 'darkorange': return new Color(new RGBA(255, 140, 0, 1)); + case 'darkorchid': return new Color(new RGBA(153, 50, 204, 1)); + case 'darkred': return new Color(new RGBA(139, 0, 0, 1)); + case 'darksalmon': return new Color(new RGBA(233, 150, 122, 1)); + case 'darkseagreen': return new Color(new RGBA(143, 188, 143, 1)); + case 'darkslateblue': return new Color(new RGBA(72, 61, 139, 1)); + case 'darkslategray': return new Color(new RGBA(47, 79, 79, 1)); + case 'darkslategrey': return new Color(new RGBA(47, 79, 79, 1)); + case 'darkturquoise': return new Color(new RGBA(0, 206, 209, 1)); + case 'darkviolet': return new Color(new RGBA(148, 0, 211, 1)); + case 'deeppink': return new Color(new RGBA(255, 20, 147, 1)); + case 'deepskyblue': return new Color(new RGBA(0, 191, 255, 1)); + case 'dimgray': return new Color(new RGBA(105, 105, 105, 1)); + case 'dimgrey': return new Color(new RGBA(105, 105, 105, 1)); + case 'dodgerblue': return new Color(new RGBA(30, 144, 255, 1)); + case 'firebrick': return new Color(new RGBA(178, 34, 34, 1)); + case 'floralwhite': return new Color(new RGBA(255, 250, 240, 1)); + case 'forestgreen': return new Color(new RGBA(34, 139, 34, 1)); + case 'fuchsia': return new Color(new RGBA(255, 0, 255, 1)); + case 'gainsboro': return new Color(new RGBA(220, 220, 220, 1)); + case 'ghostwhite': return new Color(new RGBA(248, 248, 255, 1)); + case 'gold': return new Color(new RGBA(255, 215, 0, 1)); + case 'goldenrod': return new Color(new RGBA(218, 165, 32, 1)); + case 'gray': return new Color(new RGBA(128, 128, 128, 1)); + case 'green': return new Color(new RGBA(0, 128, 0, 1)); + case 'greenyellow': return new Color(new RGBA(173, 255, 47, 1)); + case 'grey': return new Color(new RGBA(128, 128, 128, 1)); + case 'honeydew': return new Color(new RGBA(240, 255, 240, 1)); + case 'hotpink': return new Color(new RGBA(255, 105, 180, 1)); + case 'indianred': return new Color(new RGBA(205, 92, 92, 1)); + case 'indigo': return new Color(new RGBA(75, 0, 130, 1)); + case 'ivory': return new Color(new RGBA(255, 255, 240, 1)); + case 'khaki': return new Color(new RGBA(240, 230, 140, 1)); + case 'lavender': return new Color(new RGBA(230, 230, 250, 1)); + case 'lavenderblush': return new Color(new RGBA(255, 240, 245, 1)); + case 'lawngreen': return new Color(new RGBA(124, 252, 0, 1)); + case 'lemonchiffon': return new Color(new RGBA(255, 250, 205, 1)); + case 'lightblue': return new Color(new RGBA(173, 216, 230, 1)); + case 'lightcoral': return new Color(new RGBA(240, 128, 128, 1)); + case 'lightcyan': return new Color(new RGBA(224, 255, 255, 1)); + case 'lightgoldenrodyellow': return new Color(new RGBA(250, 250, 210, 1)); + case 'lightgray': return new Color(new RGBA(211, 211, 211, 1)); + case 'lightgreen': return new Color(new RGBA(144, 238, 144, 1)); + case 'lightgrey': return new Color(new RGBA(211, 211, 211, 1)); + case 'lightpink': return new Color(new RGBA(255, 182, 193, 1)); + case 'lightsalmon': return new Color(new RGBA(255, 160, 122, 1)); + case 'lightseagreen': return new Color(new RGBA(32, 178, 170, 1)); + case 'lightskyblue': return new Color(new RGBA(135, 206, 250, 1)); + case 'lightslategray': return new Color(new RGBA(119, 136, 153, 1)); + case 'lightslategrey': return new Color(new RGBA(119, 136, 153, 1)); + case 'lightsteelblue': return new Color(new RGBA(176, 196, 222, 1)); + case 'lightyellow': return new Color(new RGBA(255, 255, 224, 1)); + case 'lime': return new Color(new RGBA(0, 255, 0, 1)); + case 'limegreen': return new Color(new RGBA(50, 205, 50, 1)); + case 'linen': return new Color(new RGBA(250, 240, 230, 1)); + case 'magenta': return new Color(new RGBA(255, 0, 255, 1)); + case 'maroon': return new Color(new RGBA(128, 0, 0, 1)); + case 'mediumaquamarine': return new Color(new RGBA(102, 205, 170, 1)); + case 'mediumblue': return new Color(new RGBA(0, 0, 205, 1)); + case 'mediumorchid': return new Color(new RGBA(186, 85, 211, 1)); + case 'mediumpurple': return new Color(new RGBA(147, 112, 219, 1)); + case 'mediumseagreen': return new Color(new RGBA(60, 179, 113, 1)); + case 'mediumslateblue': return new Color(new RGBA(123, 104, 238, 1)); + case 'mediumspringgreen': return new Color(new RGBA(0, 250, 154, 1)); + case 'mediumturquoise': return new Color(new RGBA(72, 209, 204, 1)); + case 'mediumvioletred': return new Color(new RGBA(199, 21, 133, 1)); + case 'midnightblue': return new Color(new RGBA(25, 25, 112, 1)); + case 'mintcream': return new Color(new RGBA(245, 255, 250, 1)); + case 'mistyrose': return new Color(new RGBA(255, 228, 225, 1)); + case 'moccasin': return new Color(new RGBA(255, 228, 181, 1)); + case 'navajowhite': return new Color(new RGBA(255, 222, 173, 1)); + case 'navy': return new Color(new RGBA(0, 0, 128, 1)); + case 'oldlace': return new Color(new RGBA(253, 245, 230, 1)); + case 'olive': return new Color(new RGBA(128, 128, 0, 1)); + case 'olivedrab': return new Color(new RGBA(107, 142, 35, 1)); + case 'orange': return new Color(new RGBA(255, 165, 0, 1)); + case 'orangered': return new Color(new RGBA(255, 69, 0, 1)); + case 'orchid': return new Color(new RGBA(218, 112, 214, 1)); + case 'palegoldenrod': return new Color(new RGBA(238, 232, 170, 1)); + case 'palegreen': return new Color(new RGBA(152, 251, 152, 1)); + case 'paleturquoise': return new Color(new RGBA(175, 238, 238, 1)); + case 'palevioletred': return new Color(new RGBA(219, 112, 147, 1)); + case 'papayawhip': return new Color(new RGBA(255, 239, 213, 1)); + case 'peachpuff': return new Color(new RGBA(255, 218, 185, 1)); + case 'peru': return new Color(new RGBA(205, 133, 63, 1)); + case 'pink': return new Color(new RGBA(255, 192, 203, 1)); + case 'plum': return new Color(new RGBA(221, 160, 221, 1)); + case 'powderblue': return new Color(new RGBA(176, 224, 230, 1)); + case 'purple': return new Color(new RGBA(128, 0, 128, 1)); + case 'rebeccapurple': return new Color(new RGBA(102, 51, 153, 1)); + case 'red': return new Color(new RGBA(255, 0, 0, 1)); + case 'rosybrown': return new Color(new RGBA(188, 143, 143, 1)); + case 'royalblue': return new Color(new RGBA(65, 105, 225, 1)); + case 'saddlebrown': return new Color(new RGBA(139, 69, 19, 1)); + case 'salmon': return new Color(new RGBA(250, 128, 114, 1)); + case 'sandybrown': return new Color(new RGBA(244, 164, 96, 1)); + case 'seagreen': return new Color(new RGBA(46, 139, 87, 1)); + case 'seashell': return new Color(new RGBA(255, 245, 238, 1)); + case 'sienna': return new Color(new RGBA(160, 82, 45, 1)); + case 'silver': return new Color(new RGBA(192, 192, 192, 1)); + case 'skyblue': return new Color(new RGBA(135, 206, 235, 1)); + case 'slateblue': return new Color(new RGBA(106, 90, 205, 1)); + case 'slategray': return new Color(new RGBA(112, 128, 144, 1)); + case 'slategrey': return new Color(new RGBA(112, 128, 144, 1)); + case 'snow': return new Color(new RGBA(255, 250, 250, 1)); + case 'springgreen': return new Color(new RGBA(0, 255, 127, 1)); + case 'steelblue': return new Color(new RGBA(70, 130, 180, 1)); + case 'tan': return new Color(new RGBA(210, 180, 140, 1)); + case 'teal': return new Color(new RGBA(0, 128, 128, 1)); + case 'thistle': return new Color(new RGBA(216, 191, 216, 1)); + case 'tomato': return new Color(new RGBA(255, 99, 71, 1)); + case 'turquoise': return new Color(new RGBA(64, 224, 208, 1)); + case 'violet': return new Color(new RGBA(238, 130, 238, 1)); + case 'wheat': return new Color(new RGBA(245, 222, 179, 1)); + case 'white': return new Color(new RGBA(255, 255, 255, 1)); + case 'whitesmoke': return new Color(new RGBA(245, 245, 245, 1)); + case 'yellow': return new Color(new RGBA(255, 255, 0, 1)); + case 'yellowgreen': return new Color(new RGBA(154, 205, 50, 1)); + default: return null; + } } /** diff --git a/src/vs/base/test/common/color.test.ts b/src/vs/base/test/common/color.test.ts index 3ae8353bcdff3..ca5db568ca67f 100644 --- a/src/vs/base/test/common/color.test.ts +++ b/src/vs/base/test/common/color.test.ts @@ -210,9 +210,162 @@ suite('Color', () => { assert.deepStrictEqual(Color.Format.CSS.parse('#'), null); assert.deepStrictEqual(Color.Format.CSS.parse('#0102030'), null); }); + test('transparent', () => { assert.deepStrictEqual(Color.Format.CSS.parse('transparent'), new Color(new RGBA(0, 0, 0, 0))); }); + + test('named keyword', () => { + assert.deepStrictEqual(Color.Format.CSS.parse('aliceblue')!.rgba, new RGBA(240, 248, 255, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('antiquewhite')!.rgba, new RGBA(250, 235, 215, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('aqua')!.rgba, new RGBA(0, 255, 255, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('aquamarine')!.rgba, new RGBA(127, 255, 212, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('azure')!.rgba, new RGBA(240, 255, 255, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('beige')!.rgba, new RGBA(245, 245, 220, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('bisque')!.rgba, new RGBA(255, 228, 196, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('black')!.rgba, new RGBA(0, 0, 0, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('blanchedalmond')!.rgba, new RGBA(255, 235, 205, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('blue')!.rgba, new RGBA(0, 0, 255, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('blueviolet')!.rgba, new RGBA(138, 43, 226, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('brown')!.rgba, new RGBA(165, 42, 42, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('burlywood')!.rgba, new RGBA(222, 184, 135, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('cadetblue')!.rgba, new RGBA(95, 158, 160, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('chartreuse')!.rgba, new RGBA(127, 255, 0, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('chocolate')!.rgba, new RGBA(210, 105, 30, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('coral')!.rgba, new RGBA(255, 127, 80, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('cornflowerblue')!.rgba, new RGBA(100, 149, 237, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('cornsilk')!.rgba, new RGBA(255, 248, 220, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('crimson')!.rgba, new RGBA(220, 20, 60, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('cyan')!.rgba, new RGBA(0, 255, 255, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('darkblue')!.rgba, new RGBA(0, 0, 139, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('darkcyan')!.rgba, new RGBA(0, 139, 139, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('darkgoldenrod')!.rgba, new RGBA(184, 134, 11, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('darkgray')!.rgba, new RGBA(169, 169, 169, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('darkgreen')!.rgba, new RGBA(0, 100, 0, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('darkgrey')!.rgba, new RGBA(169, 169, 169, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('darkkhaki')!.rgba, new RGBA(189, 183, 107, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('darkmagenta')!.rgba, new RGBA(139, 0, 139, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('darkolivegreen')!.rgba, new RGBA(85, 107, 47, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('darkorange')!.rgba, new RGBA(255, 140, 0, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('darkorchid')!.rgba, new RGBA(153, 50, 204, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('darkred')!.rgba, new RGBA(139, 0, 0, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('darksalmon')!.rgba, new RGBA(233, 150, 122, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('darkseagreen')!.rgba, new RGBA(143, 188, 143, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('darkslateblue')!.rgba, new RGBA(72, 61, 139, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('darkslategray')!.rgba, new RGBA(47, 79, 79, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('darkslategrey')!.rgba, new RGBA(47, 79, 79, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('darkturquoise')!.rgba, new RGBA(0, 206, 209, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('darkviolet')!.rgba, new RGBA(148, 0, 211, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('deeppink')!.rgba, new RGBA(255, 20, 147, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('deepskyblue')!.rgba, new RGBA(0, 191, 255, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('dimgray')!.rgba, new RGBA(105, 105, 105, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('dimgrey')!.rgba, new RGBA(105, 105, 105, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('dodgerblue')!.rgba, new RGBA(30, 144, 255, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('firebrick')!.rgba, new RGBA(178, 34, 34, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('floralwhite')!.rgba, new RGBA(255, 250, 240, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('forestgreen')!.rgba, new RGBA(34, 139, 34, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('fuchsia')!.rgba, new RGBA(255, 0, 255, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('gainsboro')!.rgba, new RGBA(220, 220, 220, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('ghostwhite')!.rgba, new RGBA(248, 248, 255, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('gold')!.rgba, new RGBA(255, 215, 0, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('goldenrod')!.rgba, new RGBA(218, 165, 32, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('gray')!.rgba, new RGBA(128, 128, 128, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('green')!.rgba, new RGBA(0, 128, 0, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('greenyellow')!.rgba, new RGBA(173, 255, 47, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('grey')!.rgba, new RGBA(128, 128, 128, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('honeydew')!.rgba, new RGBA(240, 255, 240, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('hotpink')!.rgba, new RGBA(255, 105, 180, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('indianred')!.rgba, new RGBA(205, 92, 92, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('indigo')!.rgba, new RGBA(75, 0, 130, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('ivory')!.rgba, new RGBA(255, 255, 240, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('khaki')!.rgba, new RGBA(240, 230, 140, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('lavender')!.rgba, new RGBA(230, 230, 250, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('lavenderblush')!.rgba, new RGBA(255, 240, 245, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('lawngreen')!.rgba, new RGBA(124, 252, 0, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('lemonchiffon')!.rgba, new RGBA(255, 250, 205, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('lightblue')!.rgba, new RGBA(173, 216, 230, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('lightcoral')!.rgba, new RGBA(240, 128, 128, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('lightcyan')!.rgba, new RGBA(224, 255, 255, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('lightgoldenrodyellow')!.rgba, new RGBA(250, 250, 210, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('lightgray')!.rgba, new RGBA(211, 211, 211, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('lightgreen')!.rgba, new RGBA(144, 238, 144, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('lightgrey')!.rgba, new RGBA(211, 211, 211, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('lightpink')!.rgba, new RGBA(255, 182, 193, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('lightsalmon')!.rgba, new RGBA(255, 160, 122, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('lightseagreen')!.rgba, new RGBA(32, 178, 170, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('lightskyblue')!.rgba, new RGBA(135, 206, 250, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('lightslategray')!.rgba, new RGBA(119, 136, 153, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('lightslategrey')!.rgba, new RGBA(119, 136, 153, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('lightsteelblue')!.rgba, new RGBA(176, 196, 222, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('lightyellow')!.rgba, new RGBA(255, 255, 224, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('lime')!.rgba, new RGBA(0, 255, 0, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('limegreen')!.rgba, new RGBA(50, 205, 50, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('linen')!.rgba, new RGBA(250, 240, 230, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('magenta')!.rgba, new RGBA(255, 0, 255, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('maroon')!.rgba, new RGBA(128, 0, 0, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('mediumaquamarine')!.rgba, new RGBA(102, 205, 170, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('mediumblue')!.rgba, new RGBA(0, 0, 205, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('mediumorchid')!.rgba, new RGBA(186, 85, 211, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('mediumpurple')!.rgba, new RGBA(147, 112, 219, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('mediumseagreen')!.rgba, new RGBA(60, 179, 113, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('mediumslateblue')!.rgba, new RGBA(123, 104, 238, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('mediumspringgreen')!.rgba, new RGBA(0, 250, 154, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('mediumturquoise')!.rgba, new RGBA(72, 209, 204, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('mediumvioletred')!.rgba, new RGBA(199, 21, 133, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('midnightblue')!.rgba, new RGBA(25, 25, 112, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('mintcream')!.rgba, new RGBA(245, 255, 250, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('mistyrose')!.rgba, new RGBA(255, 228, 225, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('moccasin')!.rgba, new RGBA(255, 228, 181, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('navajowhite')!.rgba, new RGBA(255, 222, 173, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('navy')!.rgba, new RGBA(0, 0, 128, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('oldlace')!.rgba, new RGBA(253, 245, 230, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('olive')!.rgba, new RGBA(128, 128, 0, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('olivedrab')!.rgba, new RGBA(107, 142, 35, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('orange')!.rgba, new RGBA(255, 165, 0, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('orangered')!.rgba, new RGBA(255, 69, 0, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('orchid')!.rgba, new RGBA(218, 112, 214, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('palegoldenrod')!.rgba, new RGBA(238, 232, 170, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('palegreen')!.rgba, new RGBA(152, 251, 152, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('paleturquoise')!.rgba, new RGBA(175, 238, 238, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('palevioletred')!.rgba, new RGBA(219, 112, 147, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('papayawhip')!.rgba, new RGBA(255, 239, 213, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('peachpuff')!.rgba, new RGBA(255, 218, 185, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('peru')!.rgba, new RGBA(205, 133, 63, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('pink')!.rgba, new RGBA(255, 192, 203, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('plum')!.rgba, new RGBA(221, 160, 221, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('powderblue')!.rgba, new RGBA(176, 224, 230, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('purple')!.rgba, new RGBA(128, 0, 128, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('rebeccapurple')!.rgba, new RGBA(102, 51, 153, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('red')!.rgba, new RGBA(255, 0, 0, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('rosybrown')!.rgba, new RGBA(188, 143, 143, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('royalblue')!.rgba, new RGBA(65, 105, 225, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('saddlebrown')!.rgba, new RGBA(139, 69, 19, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('salmon')!.rgba, new RGBA(250, 128, 114, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('sandybrown')!.rgba, new RGBA(244, 164, 96, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('seagreen')!.rgba, new RGBA(46, 139, 87, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('seashell')!.rgba, new RGBA(255, 245, 238, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('sienna')!.rgba, new RGBA(160, 82, 45, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('silver')!.rgba, new RGBA(192, 192, 192, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('skyblue')!.rgba, new RGBA(135, 206, 235, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('slateblue')!.rgba, new RGBA(106, 90, 205, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('slategray')!.rgba, new RGBA(112, 128, 144, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('slategrey')!.rgba, new RGBA(112, 128, 144, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('snow')!.rgba, new RGBA(255, 250, 250, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('springgreen')!.rgba, new RGBA(0, 255, 127, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('steelblue')!.rgba, new RGBA(70, 130, 180, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('tan')!.rgba, new RGBA(210, 180, 140, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('teal')!.rgba, new RGBA(0, 128, 128, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('thistle')!.rgba, new RGBA(216, 191, 216, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('tomato')!.rgba, new RGBA(255, 99, 71, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('turquoise')!.rgba, new RGBA(64, 224, 208, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('violet')!.rgba, new RGBA(238, 130, 238, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('wheat')!.rgba, new RGBA(245, 222, 179, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('white')!.rgba, new RGBA(255, 255, 255, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('whitesmoke')!.rgba, new RGBA(245, 245, 245, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('yellow')!.rgba, new RGBA(255, 255, 0, 1)); + assert.deepStrictEqual(Color.Format.CSS.parse('yellowgreen')!.rgba, new RGBA(154, 205, 50, 1)); + }); + test('hex-color', () => { // somewhat valid assert.deepStrictEqual(Color.Format.CSS.parse('#FFFFG0')!.rgba, new RGBA(255, 255, 0, 1)); From b5c3a7f91362dba398912e4ac5dc1750864e0c89 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 21 Nov 2024 11:40:45 -0800 Subject: [PATCH 017/119] Fix transparent color test --- src/vs/base/test/common/color.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/base/test/common/color.test.ts b/src/vs/base/test/common/color.test.ts index ca5db568ca67f..5410575cc3304 100644 --- a/src/vs/base/test/common/color.test.ts +++ b/src/vs/base/test/common/color.test.ts @@ -212,7 +212,7 @@ suite('Color', () => { }); test('transparent', () => { - assert.deepStrictEqual(Color.Format.CSS.parse('transparent'), new Color(new RGBA(0, 0, 0, 0))); + assert.deepStrictEqual(Color.Format.CSS.parse('transparent')!.rgba, new RGBA(0, 0, 0, 0)); }); test('named keyword', () => { From 8f65102dbf76b8cf35d1143e21350481e13395ca Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 22 Nov 2024 06:55:35 -0800 Subject: [PATCH 018/119] Fix issue with caching ignoring charMetadata --- src/vs/base/common/color.ts | 13 ++++++++ .../browser/gpu/atlas/textureAtlasPage.ts | 3 +- .../browser/gpu/fullFileRenderStrategy.ts | 32 ++++--------------- .../browser/gpu/raster/glyphRasterizer.ts | 12 +++---- src/vs/editor/browser/gpu/raster/raster.ts | 8 ----- 5 files changed, 27 insertions(+), 41 deletions(-) diff --git a/src/vs/base/common/color.ts b/src/vs/base/common/color.ts index 02d98667fb71d..67eeeec757a3e 100644 --- a/src/vs/base/common/color.ts +++ b/src/vs/base/common/color.ts @@ -535,6 +535,19 @@ export class Color { return this._toString; } + private _toNumber24Bit?: number; + toNumber24Bit(): number { + if (!this._toNumber24Bit) { + this._toNumber24Bit = ( + this.rgba.r /* */ << 24 | + this.rgba.g /* */ << 16 | + this.rgba.b /* */ << 8 | + this.rgba.a * 0xFF << 0 + ) >>> 0; + } + return this._toNumber24Bit; + } + static getLighterColor(of: Color, relative: Color, factor?: number): Color { if (of.isLighterThan(relative)) { return of; diff --git a/src/vs/editor/browser/gpu/atlas/textureAtlasPage.ts b/src/vs/editor/browser/gpu/atlas/textureAtlasPage.ts index cbda9352a3bc8..9c09181751a7a 100644 --- a/src/vs/editor/browser/gpu/atlas/textureAtlasPage.ts +++ b/src/vs/editor/browser/gpu/atlas/textureAtlasPage.ts @@ -100,7 +100,8 @@ export class TextureAtlasPage extends Disposable implements IReadableTextureAtla if (this._logService.getLevel() === LogLevel.Trace) { this._logService.trace('New glyph', { chars, - metadata: tokenMetadata, + tokenMetadata, + charMetadata, rasterizedGlyph, glyph }); diff --git a/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts b/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts index b1c99760b28f5..bfb27a81d332b 100644 --- a/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts +++ b/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts @@ -8,7 +8,6 @@ import { BugIndicatingError } from '../../../base/common/errors.js'; import { MandatoryMutableDisposable } from '../../../base/common/lifecycle.js'; import { EditorOption } from '../../common/config/editorOptions.js'; import { CursorColumns } from '../../common/core/cursorColumns.js'; -import { MetadataConsts } from '../../common/encodedTokenAttributes.js'; import type { IViewLineTokens } from '../../common/tokens/lineTokens.js'; import { ViewEventHandler } from '../../common/viewEventHandler.js'; import { ViewEventType, type ViewConfigurationChangedEvent, type ViewDecorationsChangedEvent, type ViewLinesChangedEvent, type ViewLinesDeletedEvent, type ViewLinesInsertedEvent, type ViewScrollChangedEvent, type ViewTokensChangedEvent, type ViewZonesChangedEvent } from '../../common/viewEvents.js'; @@ -23,10 +22,8 @@ import { GPULifecycle } from './gpuDisposable.js'; import { quadVertices } from './gpuUtils.js'; import { GlyphRasterizer } from './raster/glyphRasterizer.js'; import { ViewGpuContext } from './viewGpuContext.js'; -import { GpuCharMetadata } from './raster/raster.js'; import { Color } from '../../../base/common/color.js'; - const enum Constants { IndicesPerCell = 6, } @@ -141,7 +138,7 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend } public override onDecorationsChanged(e: ViewDecorationsChangedEvent): boolean { - // TODO: Don't clear all lines + // TODO: Don't clear all cells if we can avoid it this._invalidateAllLines(); return true; } @@ -225,14 +222,14 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend } reset() { + this._invalidateAllLines(); for (const bufferIndex of [0, 1]) { // Zero out buffer and upload to GPU to prevent stale rows from rendering const buffer = new Float32Array(this._cellValueBuffers[bufferIndex]); buffer.fill(0, 0, buffer.length); this._device.queue.writeBuffer(this._cellBindBuffer, 0, buffer.buffer, 0, buffer.byteLength); - this._upToDateLines[bufferIndex].clear(); } - this._visibleObjectCount = 0; + this._finalRenderedLine = 0; } update(viewportData: ViewportData, viewLineOptions: ViewLineOptions): number { @@ -279,7 +276,6 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend const queuedBufferUpdates = this._queuedBufferUpdates[this._activeDoubleBufferIndex]; while (queuedBufferUpdates.length) { const e = queuedBufferUpdates.shift()!; - switch (e.type) { case ViewEventType.ViewConfigurationChanged: { // TODO: Refine the cases for when we throw away all the data @@ -368,9 +364,8 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend charMetadata = 0; // TODO: We'd want to optimize pulling the decorations in order - // HACK: Temporary replace char to demonstrate inline decorations const cellDecorations = inlineDecorations.filter(decoration => { - // TODO: Why does Range.containsPosition and Range.strictContainsPosition not work here? + // This is Range.strictContainsPosition except it's working at the cell level. if (y < decoration.range.startLineNumber || y > decoration.range.endLineNumber) { return false; } @@ -400,28 +395,13 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend for (const [key, value] of inlineStyles.entries()) { switch (key) { - case 'text-decoration-line': { - charMetadata |= MetadataConsts.STRIKETHROUGH_MASK; - break; - } - case 'text-decoration-thickness': - case 'text-decoration-style': - case 'text-decoration-color': { - // HACK: Ignore for now to avoid throwing - break; - } case 'color': { // TODO: This parsing/error handling should move into canRender so fallback to DOM works const parsedColor = Color.Format.CSS.parse(value); if (!parsedColor) { - throw new Error('Invalid color format ' + value); - } - const rgb = parsedColor.rgba.r << 16 | parsedColor.rgba.g << 8 | parsedColor.rgba.b; - charMetadata |= ((rgb << GpuCharMetadata.FOREGROUND_OFFSET) & GpuCharMetadata.FOREGROUND_MASK) >>> 0; - // TODO: _foreground_ opacity should not be applied to regular opacity - if (parsedColor.rgba.a < 1) { - charMetadata |= ((parsedColor.rgba.a * 0xFF << GpuCharMetadata.OPACITY_OFFSET) & GpuCharMetadata.OPACITY_MASK) >>> 0; + throw new BugIndicatingError('Invalid color format ' + value); } + charMetadata = parsedColor.toNumber24Bit(); break; } default: throw new BugIndicatingError('Unexpected inline decoration style'); diff --git a/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts b/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts index efec3211c9a7c..a0ae8927b832d 100644 --- a/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts +++ b/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts @@ -9,7 +9,7 @@ import { Disposable } from '../../../../base/common/lifecycle.js'; import { StringBuilder } from '../../../common/core/stringBuilder.js'; import { FontStyle, TokenMetadata } from '../../../common/encodedTokenAttributes.js'; import { ensureNonNullable } from '../gpuUtils.js'; -import { GpuCharMetadata, type IBoundingBox, type IGlyphRasterizer, type IRasterizedGlyph } from './raster.js'; +import { type IBoundingBox, type IGlyphRasterizer, type IRasterizedGlyph } from './raster.js'; let nextId = 0; @@ -37,7 +37,7 @@ export class GlyphRasterizer extends Disposable implements IGlyphRasterizer { y: 0, } }; - private _workGlyphConfig: { chars: string | undefined; metadata: number } = { chars: undefined, metadata: 0 }; + private _workGlyphConfig: { chars: string | undefined; tokenMetadata: number; charMetadata: number } = { chars: undefined, tokenMetadata: 0, charMetadata: 0 }; constructor( readonly fontSize: number, @@ -75,11 +75,12 @@ export class GlyphRasterizer extends Disposable implements IGlyphRasterizer { // Check if the last glyph matches the config, reuse if so. This helps avoid unnecessary // work when the rasterizer is called multiple times like when the glyph doesn't fit into a // page. - if (this._workGlyphConfig.chars === chars && this._workGlyphConfig.metadata === tokenMetadata) { + if (this._workGlyphConfig.chars === chars && this._workGlyphConfig.tokenMetadata === tokenMetadata && this._workGlyphConfig.charMetadata === charMetadata) { return this._workGlyph; } this._workGlyphConfig.chars = chars; - this._workGlyphConfig.metadata = tokenMetadata; + this._workGlyphConfig.tokenMetadata = tokenMetadata; + this._workGlyphConfig.charMetadata = charMetadata; return this._rasterizeGlyph(chars, tokenMetadata, charMetadata, colorMap); } @@ -117,8 +118,7 @@ export class GlyphRasterizer extends Disposable implements IGlyphRasterizer { const originX = devicePixelFontSize; const originY = devicePixelFontSize; if (charMetadata) { - const fg = (charMetadata & GpuCharMetadata.FOREGROUND_MASK) >> GpuCharMetadata.FOREGROUND_OFFSET; - this._ctx.fillStyle = `#${fg.toString(16).padStart(6, '0')}`; + this._ctx.fillStyle = `#${charMetadata.toString(16).padStart(8, '0')}`; } else { this._ctx.fillStyle = colorMap[TokenMetadata.getForeground(metadata)]; } diff --git a/src/vs/editor/browser/gpu/raster/raster.ts b/src/vs/editor/browser/gpu/raster/raster.ts index d83b55665dbf5..c86b1649e34b7 100644 --- a/src/vs/editor/browser/gpu/raster/raster.ts +++ b/src/vs/editor/browser/gpu/raster/raster.ts @@ -65,11 +65,3 @@ export interface IRasterizedGlyph { */ originOffset: { x: number; y: number }; } - -export const enum GpuCharMetadata { - FOREGROUND_MASK /* */ = 0b00000000_11111111_11111111_11111111, - OPACITY_MASK /* */ = 0b11111111_00000000_00000000_00000000, - - FOREGROUND_OFFSET = 0, - OPACITY_OFFSET = 24, -} From 9b4c43bf852c0af2accfe60257862cded58b064a Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 22 Nov 2024 06:56:49 -0800 Subject: [PATCH 019/119] Add tests for toString and toNumber24Bit --- src/vs/base/test/common/color.test.ts | 74 +++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/src/vs/base/test/common/color.test.ts b/src/vs/base/test/common/color.test.ts index 5410575cc3304..c0f439d744ed4 100644 --- a/src/vs/base/test/common/color.test.ts +++ b/src/vs/base/test/common/color.test.ts @@ -81,6 +81,80 @@ suite('Color', () => { assert.deepStrictEqual(new Color(new RGBA(0, 0, 0, 0.58)).blend(new Color(new RGBA(255, 255, 255, 0.33))), new Color(new RGBA(49, 49, 49, 0.719))); }); + suite('toString', () => { + test('alpha channel', () => { + assert.deepStrictEqual(Color.fromHex('#00000000').toString(), 'rgba(0, 0, 0, 0)'); + assert.deepStrictEqual(Color.fromHex('#00000080').toString(), 'rgba(0, 0, 0, 0.5)'); + assert.deepStrictEqual(Color.fromHex('#000000FF').toString(), '#000000'); + }); + + test('opaque', () => { + assert.deepStrictEqual(Color.fromHex('#000000').toString().toUpperCase(), '#000000'.toUpperCase()); + assert.deepStrictEqual(Color.fromHex('#FFFFFF').toString().toUpperCase(), '#FFFFFF'.toUpperCase()); + assert.deepStrictEqual(Color.fromHex('#FF0000').toString().toUpperCase(), '#FF0000'.toUpperCase()); + assert.deepStrictEqual(Color.fromHex('#00FF00').toString().toUpperCase(), '#00FF00'.toUpperCase()); + assert.deepStrictEqual(Color.fromHex('#0000FF').toString().toUpperCase(), '#0000FF'.toUpperCase()); + assert.deepStrictEqual(Color.fromHex('#FFFF00').toString().toUpperCase(), '#FFFF00'.toUpperCase()); + assert.deepStrictEqual(Color.fromHex('#00FFFF').toString().toUpperCase(), '#00FFFF'.toUpperCase()); + assert.deepStrictEqual(Color.fromHex('#FF00FF').toString().toUpperCase(), '#FF00FF'.toUpperCase()); + assert.deepStrictEqual(Color.fromHex('#C0C0C0').toString().toUpperCase(), '#C0C0C0'.toUpperCase()); + assert.deepStrictEqual(Color.fromHex('#808080').toString().toUpperCase(), '#808080'.toUpperCase()); + assert.deepStrictEqual(Color.fromHex('#800000').toString().toUpperCase(), '#800000'.toUpperCase()); + assert.deepStrictEqual(Color.fromHex('#808000').toString().toUpperCase(), '#808000'.toUpperCase()); + assert.deepStrictEqual(Color.fromHex('#008000').toString().toUpperCase(), '#008000'.toUpperCase()); + assert.deepStrictEqual(Color.fromHex('#800080').toString().toUpperCase(), '#800080'.toUpperCase()); + assert.deepStrictEqual(Color.fromHex('#008080').toString().toUpperCase(), '#008080'.toUpperCase()); + assert.deepStrictEqual(Color.fromHex('#000080').toString().toUpperCase(), '#000080'.toUpperCase()); + assert.deepStrictEqual(Color.fromHex('#010203').toString().toUpperCase(), '#010203'.toUpperCase()); + assert.deepStrictEqual(Color.fromHex('#040506').toString().toUpperCase(), '#040506'.toUpperCase()); + assert.deepStrictEqual(Color.fromHex('#070809').toString().toUpperCase(), '#070809'.toUpperCase()); + assert.deepStrictEqual(Color.fromHex('#0a0A0a').toString().toUpperCase(), '#0a0A0a'.toUpperCase()); + assert.deepStrictEqual(Color.fromHex('#0b0B0b').toString().toUpperCase(), '#0b0B0b'.toUpperCase()); + assert.deepStrictEqual(Color.fromHex('#0c0C0c').toString().toUpperCase(), '#0c0C0c'.toUpperCase()); + assert.deepStrictEqual(Color.fromHex('#0d0D0d').toString().toUpperCase(), '#0d0D0d'.toUpperCase()); + assert.deepStrictEqual(Color.fromHex('#0e0E0e').toString().toUpperCase(), '#0e0E0e'.toUpperCase()); + assert.deepStrictEqual(Color.fromHex('#0f0F0f').toString().toUpperCase(), '#0f0F0f'.toUpperCase()); + assert.deepStrictEqual(Color.fromHex('#a0A0a0').toString().toUpperCase(), '#a0A0a0'.toUpperCase()); + }); + }); + + suite('toNumber24Bit', () => { + test('alpha channel', () => { + assert.deepStrictEqual(Color.fromHex('#00000000').toNumber24Bit(), 0x00000000); + assert.deepStrictEqual(Color.fromHex('#00000080').toNumber24Bit(), 0x00000080); + assert.deepStrictEqual(Color.fromHex('#000000FF').toNumber24Bit(), 0x000000FF); + }); + + test('opaque', () => { + assert.deepStrictEqual(Color.fromHex('#000000').toNumber24Bit(), 0x000000FF); + assert.deepStrictEqual(Color.fromHex('#FFFFFF').toNumber24Bit(), 0xFFFFFFFF); + assert.deepStrictEqual(Color.fromHex('#FF0000').toNumber24Bit(), 0xFF0000FF); + assert.deepStrictEqual(Color.fromHex('#00FF00').toNumber24Bit(), 0x00FF00FF); + assert.deepStrictEqual(Color.fromHex('#0000FF').toNumber24Bit(), 0x0000FFFF); + assert.deepStrictEqual(Color.fromHex('#FFFF00').toNumber24Bit(), 0xFFFF00FF); + assert.deepStrictEqual(Color.fromHex('#00FFFF').toNumber24Bit(), 0x00FFFFFF); + assert.deepStrictEqual(Color.fromHex('#FF00FF').toNumber24Bit(), 0xFF00FFFF); + assert.deepStrictEqual(Color.fromHex('#C0C0C0').toNumber24Bit(), 0xC0C0C0FF); + assert.deepStrictEqual(Color.fromHex('#808080').toNumber24Bit(), 0x808080FF); + assert.deepStrictEqual(Color.fromHex('#800000').toNumber24Bit(), 0x800000FF); + assert.deepStrictEqual(Color.fromHex('#808000').toNumber24Bit(), 0x808000FF); + assert.deepStrictEqual(Color.fromHex('#008000').toNumber24Bit(), 0x008000FF); + assert.deepStrictEqual(Color.fromHex('#800080').toNumber24Bit(), 0x800080FF); + assert.deepStrictEqual(Color.fromHex('#008080').toNumber24Bit(), 0x008080FF); + assert.deepStrictEqual(Color.fromHex('#000080').toNumber24Bit(), 0x000080FF); + assert.deepStrictEqual(Color.fromHex('#010203').toNumber24Bit(), 0x010203FF); + assert.deepStrictEqual(Color.fromHex('#040506').toNumber24Bit(), 0x040506FF); + assert.deepStrictEqual(Color.fromHex('#070809').toNumber24Bit(), 0x070809FF); + assert.deepStrictEqual(Color.fromHex('#0a0A0a').toNumber24Bit(), 0x0a0A0aFF); + assert.deepStrictEqual(Color.fromHex('#0b0B0b').toNumber24Bit(), 0x0b0B0bFF); + assert.deepStrictEqual(Color.fromHex('#0c0C0c').toNumber24Bit(), 0x0c0C0cFF); + assert.deepStrictEqual(Color.fromHex('#0d0D0d').toNumber24Bit(), 0x0d0D0dFF); + assert.deepStrictEqual(Color.fromHex('#0e0E0e').toNumber24Bit(), 0x0e0E0eFF); + assert.deepStrictEqual(Color.fromHex('#0f0F0f').toNumber24Bit(), 0x0f0F0fFF); + assert.deepStrictEqual(Color.fromHex('#a0A0a0').toNumber24Bit(), 0xa0A0a0FF); + }); + }); + suite('HSLA', () => { test('HSLA.toRGBA', () => { assert.deepStrictEqual(HSLA.toRGBA(new HSLA(0, 0, 0, 0)), new RGBA(0, 0, 0, 0)); From d19030040e06af145b83616173992ca4fae8b7c9 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Thu, 21 Nov 2024 01:32:56 +0100 Subject: [PATCH 020/119] Git - only show git blame for text documents with the `file` scheme for now (#234312) --- extensions/git/src/blame.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/git/src/blame.ts b/extensions/git/src/blame.ts index e9bc84661ae82..d116922d9bf4a 100644 --- a/extensions/git/src/blame.ts +++ b/extensions/git/src/blame.ts @@ -305,7 +305,7 @@ class GitBlameEditorDecoration { } const blameInformation = this._controller.textEditorBlameInformation.get(textEditor); - if (!blameInformation) { + if (!blameInformation || textEditor.document.uri.scheme !== 'file') { textEditor.setDecorations(this._decorationType, []); return; } @@ -393,7 +393,7 @@ class GitBlameStatusBarItem { } const blameInformation = this._controller.textEditorBlameInformation.get(textEditor); - if (!blameInformation || blameInformation.length === 0) { + if (!blameInformation || blameInformation.length === 0 || textEditor.document.uri.scheme !== 'file') { this._statusBarItem.hide(); return; } From 78eed5d2bb9072167209de2eb6df4523356f8e54 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Wed, 20 Nov 2024 20:35:55 -0500 Subject: [PATCH 021/119] add support for file/folder terminal completions (#234289) --- .../src/terminalSuggestMain.ts | 207 +++++++++++------- .../workbench/api/common/extHost.api.impl.ts | 1 + .../workbench/api/common/extHost.protocol.ts | 4 +- .../api/common/extHostTerminalService.ts | 4 +- src/vs/workbench/api/common/extHostTypes.ts | 35 +++ .../browser/terminalCompletionService.ts | 141 ++++++++++-- .../suggest/browser/terminalSuggestAddon.ts | 2 +- ...e.proposed.terminalCompletionProvider.d.ts | 47 +++- 8 files changed, 334 insertions(+), 107 deletions(-) diff --git a/extensions/terminal-suggest/src/terminalSuggestMain.ts b/extensions/terminal-suggest/src/terminalSuggestMain.ts index 3e7dc44d2c58b..a8d537334e703 100644 --- a/extensions/terminal-suggest/src/terminalSuggestMain.ts +++ b/extensions/terminal-suggest/src/terminalSuggestMain.ts @@ -20,11 +20,13 @@ function getBuiltinCommands(shell: string): string[] | undefined { if (cachedCommands) { return cachedCommands; } + // fixes a bug with file/folder completions brought about by the '.' command + const filter = (cmd: string) => cmd && cmd !== '.'; const options: ExecOptionsWithStringEncoding = { encoding: 'utf-8', shell }; switch (shellType) { case 'bash': { const bashOutput = execSync('compgen -b', options); - const bashResult = bashOutput.split('\n').filter(cmd => cmd); + const bashResult = bashOutput.split('\n').filter(filter); if (bashResult.length) { cachedBuiltinCommands?.set(shellType, bashResult); return bashResult; @@ -33,7 +35,7 @@ function getBuiltinCommands(shell: string): string[] | undefined { } case 'zsh': { const zshOutput = execSync('printf "%s\\n" ${(k)builtins}', options); - const zshResult = zshOutput.split('\n').filter(cmd => cmd); + const zshResult = zshOutput.split('\n').filter(filter); if (zshResult.length) { cachedBuiltinCommands?.set(shellType, zshResult); return zshResult; @@ -43,7 +45,7 @@ function getBuiltinCommands(shell: string): string[] | undefined { // TODO: ghost text in the command line prevents // completions from working ATM for fish const fishOutput = execSync('functions -n', options); - const fishResult = fishOutput.split(', ').filter(cmd => cmd); + const fishResult = fishOutput.split(', ').filter(filter); if (fishResult.length) { cachedBuiltinCommands?.set(shellType, fishResult); return fishResult; @@ -64,122 +66,81 @@ function getBuiltinCommands(shell: string): string[] | undefined { export async function activate(context: vscode.ExtensionContext) { context.subscriptions.push(vscode.window.registerTerminalCompletionProvider({ id: 'terminal-suggest', - async provideTerminalCompletions(terminal: vscode.Terminal, terminalContext: { commandLine: string; cursorPosition: number }, token: vscode.CancellationToken): Promise { + async provideTerminalCompletions(terminal: vscode.Terminal, terminalContext: { commandLine: string; cursorPosition: number }, token: vscode.CancellationToken): Promise { if (token.isCancellationRequested) { return; } - const availableCommands = await getCommandsInPath(); - if (!availableCommands) { - return; - } - // TODO: Leverage shellType when available https://github.com/microsoft/vscode/issues/230165 const shellPath = 'shellPath' in terminal.creationOptions ? terminal.creationOptions.shellPath : vscode.env.shell; if (!shellPath) { return; } + const commandsInPath = await getCommandsInPath(); const builtinCommands = getBuiltinCommands(shellPath); - builtinCommands?.forEach(command => availableCommands.add(command)); + if (!commandsInPath || !builtinCommands) { + return; + } + const commands = [...commandsInPath, ...builtinCommands]; + const items: vscode.TerminalCompletionItem[] = []; const prefix = getPrefix(terminalContext.commandLine, terminalContext.cursorPosition); - let result: vscode.TerminalCompletionItem[] = []; - const specs = [codeCompletionSpec, codeInsidersCompletionSpec]; - for (const spec of specs) { - const specName = getLabel(spec); - if (!specName || !availableCommands.has(specName)) { - continue; - } - if (terminalContext.commandLine.startsWith(specName)) { - if ('options' in codeInsidersCompletionSpec && codeInsidersCompletionSpec.options) { - for (const option of codeInsidersCompletionSpec.options) { - const optionLabel = getLabel(option); - if (!optionLabel) { - continue; - } - if (optionLabel.startsWith(prefix) || (prefix.length > specName.length && prefix.trim() === specName)) { - result.push(createCompletionItem(terminalContext.cursorPosition, prefix, optionLabel, option.description, false, vscode.TerminalCompletionItemKind.Flag)); - } - if (option.args !== undefined) { - const args = Array.isArray(option.args) ? option.args : [option.args]; - for (const arg of args) { - if (!arg) { - continue; - } + const specs = [codeCompletionSpec, codeInsidersCompletionSpec]; + const specCompletions = await getCompletionItemsFromSpecs(specs, terminalContext, new Set(commands), prefix, token); - if (arg.template) { - // TODO: return file/folder completion items - if (arg.template === 'filepaths') { - // if (label.startsWith(prefix+\s*)) { - // result.push(FilePathCompletionItem) - // } - } else if (arg.template === 'folders') { - // if (label.startsWith(prefix+\s*)) { - // result.push(FolderPathCompletionItem) - // } - } - continue; - } + let filesRequested = specCompletions.filesRequested; + let foldersRequested = specCompletions.foldersRequested; + items.push(...specCompletions.items); - const precedingText = terminalContext.commandLine.slice(0, terminalContext.cursorPosition); - const expectedText = `${optionLabel} `; - if (arg.suggestions?.length && precedingText.includes(expectedText)) { - // there are specific suggestions to show - result = []; - const indexOfPrecedingText = terminalContext.commandLine.lastIndexOf(expectedText); - const currentPrefix = precedingText.slice(indexOfPrecedingText + expectedText.length); - for (const suggestion of arg.suggestions) { - const suggestionLabel = getLabel(suggestion); - if (suggestionLabel && suggestionLabel.startsWith(currentPrefix)) { - const hasSpaceBeforeCursor = terminalContext.commandLine[terminalContext.cursorPosition - 1] === ' '; - // prefix will be '' if there is a space before the cursor - result.push(createCompletionItem(terminalContext.cursorPosition, precedingText, suggestionLabel, arg.name, hasSpaceBeforeCursor, vscode.TerminalCompletionItemKind.Argument)); - } - } - if (result.length) { - return result; - } - } - } - } - } + if (!specCompletions.specificSuggestionsProvided) { + for (const command of commands) { + if (command.startsWith(prefix)) { + items.push(createCompletionItem(terminalContext.cursorPosition, prefix, command)); } } } - for (const command of availableCommands) { - if (command.startsWith(prefix)) { - result.push(createCompletionItem(terminalContext.cursorPosition, prefix, command)); - } - } - if (token.isCancellationRequested) { return undefined; } + const uniqueResults = new Map(); - for (const item of result) { + for (const item of items) { if (!uniqueResults.has(item.label)) { uniqueResults.set(item.label, item); } } - return uniqueResults.size ? Array.from(uniqueResults.values()) : undefined; + const resultItems = uniqueResults.size ? Array.from(uniqueResults.values()) : undefined; + + // If no completions are found, the prefix is a path, and neither files nor folders + // are going to be requested (for a specific spec's argument), show file/folder completions + const shouldShowResourceCompletions = !resultItems?.length && prefix.match(/^[./\\ ]/) && !filesRequested && !foldersRequested; + if (shouldShowResourceCompletions) { + filesRequested = true; + foldersRequested = true; + } + + if (filesRequested || foldersRequested) { + return new vscode.TerminalCompletionList(resultItems, { filesRequested, foldersRequested, cwd: terminal.shellIntegration?.cwd, pathSeparator: shellPath.includes('/') ? '/' : '\\' }); + } + return resultItems; } })); } -function getLabel(spec: Fig.Spec | Fig.Arg | Fig.Suggestion | string): string | undefined { +function getLabel(spec: Fig.Spec | Fig.Arg | Fig.Suggestion | string): string[] | undefined { if (typeof spec === 'string') { - return spec; + return [spec]; } if (typeof spec.name === 'string') { - return spec.name; + return [spec.name]; } if (!Array.isArray(spec.name) || spec.name.length === 0) { return; } - return spec.name[0]; + return spec.name; } function createCompletionItem(cursorPosition: number, prefix: string, label: string, description?: string, hasSpaceBeforeCursor?: boolean, kind?: vscode.TerminalCompletionItemKind): vscode.TerminalCompletionItem { @@ -245,3 +206,89 @@ function getPrefix(commandLine: string, cursorPosition: number): string { return match ? match[0] : ''; } +export function asArray(x: T | T[]): T[]; +export function asArray(x: T | readonly T[]): readonly T[]; +export function asArray(x: T | T[]): T[] { + return Array.isArray(x) ? x : [x]; +} + +function getCompletionItemsFromSpecs(specs: Fig.Spec[], terminalContext: { commandLine: string; cursorPosition: number }, availableCommands: Set, prefix: string, token: vscode.CancellationToken): { items: vscode.TerminalCompletionItem[]; filesRequested: boolean; foldersRequested: boolean; specificSuggestionsProvided: boolean } { + let items: vscode.TerminalCompletionItem[] = []; + let filesRequested = false; + let foldersRequested = false; + for (const spec of specs) { + const specLabels = getLabel(spec); + if (!specLabels) { + continue; + } + for (const specLabel of specLabels) { + if (!availableCommands.has(specLabel) || token.isCancellationRequested) { + continue; + } + if (terminalContext.commandLine.startsWith(specLabel)) { + if ('options' in spec && spec.options) { + for (const option of spec.options) { + const optionLabels = getLabel(option); + if (!optionLabels) { + continue; + } + for (const optionLabel of optionLabels) { + if (optionLabel.startsWith(prefix) || (prefix.length > specLabel.length && prefix.trim() === specLabel)) { + items.push(createCompletionItem(terminalContext.cursorPosition, prefix, optionLabel, option.description, false, vscode.TerminalCompletionItemKind.Flag)); + } + if (!option.args) { + continue; + } + const args = asArray(option.args); + for (const arg of args) { + if (!arg) { + continue; + } + const precedingText = terminalContext.commandLine.slice(0, terminalContext.cursorPosition + 1); + const expectedText = `${specLabel} ${optionLabel} `; + if (!precedingText.includes(expectedText)) { + continue; + } + if (arg.template) { + if (arg.template === 'filepaths') { + if (precedingText.includes(expectedText)) { + filesRequested = true; + } + } else if (arg.template === 'folders') { + if (precedingText.includes(expectedText)) { + foldersRequested = true; + } + } + } + if (arg.suggestions?.length) { + // there are specific suggestions to show + items = []; + const indexOfPrecedingText = terminalContext.commandLine.lastIndexOf(expectedText); + const currentPrefix = precedingText.slice(indexOfPrecedingText + expectedText.length); + for (const suggestion of arg.suggestions) { + const suggestionLabels = getLabel(suggestion); + if (!suggestionLabels) { + continue; + } + for (const suggestionLabel of suggestionLabels) { + if (suggestionLabel && suggestionLabel.startsWith(currentPrefix.trim())) { + const hasSpaceBeforeCursor = terminalContext.commandLine[terminalContext.cursorPosition - 1] === ' '; + // prefix will be '' if there is a space before the cursor + items.push(createCompletionItem(terminalContext.cursorPosition, precedingText, suggestionLabel, arg.name, hasSpaceBeforeCursor, vscode.TerminalCompletionItemKind.Argument)); + } + } + } + if (items.length) { + return { items, filesRequested, foldersRequested, specificSuggestionsProvided: true }; + } + } + } + } + } + } + } + } + } + return { items, filesRequested, foldersRequested, specificSuggestionsProvided: false }; +} + diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 2f20db169f796..8d2a972f62bd0 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1667,6 +1667,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I TerminalShellExecutionCommandLineConfidence: extHostTypes.TerminalShellExecutionCommandLineConfidence, TerminalCompletionItem: extHostTypes.TerminalCompletionItem, TerminalCompletionItemKind: extHostTypes.TerminalCompletionItemKind, + TerminalCompletionList: extHostTypes.TerminalCompletionList, TextDocumentSaveReason: extHostTypes.TextDocumentSaveReason, TextEdit: extHostTypes.TextEdit, SnippetTextEdit: extHostTypes.SnippetTextEdit, diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 2ddf94fa6b84d..d57fd4541a7a4 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -84,7 +84,7 @@ import { IFileQueryBuilderOptions, ITextQueryBuilderOptions } from '../../servic import * as search from '../../services/search/common/search.js'; import { TextSearchCompleteMessage } from '../../services/search/common/searchExtTypes.js'; import { ISaveProfileResult } from '../../services/userDataProfile/common/userDataProfile.js'; -import { TerminalCompletionItem, TerminalShellExecutionCommandLineConfidence } from './extHostTypes.js'; +import { TerminalCompletionItem, TerminalCompletionList, TerminalShellExecutionCommandLineConfidence } from './extHostTypes.js'; import * as tasks from './shared/tasks.js'; export interface IWorkspaceData extends IStaticWorkspaceData { @@ -2430,7 +2430,7 @@ export interface ExtHostTerminalServiceShape { $acceptDefaultProfile(profile: ITerminalProfile, automationProfile: ITerminalProfile): void; $createContributedProfileTerminal(id: string, options: ICreateContributedTerminalProfileOptions): Promise; $provideTerminalQuickFixes(id: string, matchResult: TerminalCommandMatchResultDto, token: CancellationToken): Promise | undefined>; - $provideTerminalCompletions(id: string, options: ITerminalCompletionContextDto, token: CancellationToken): Promise; + $provideTerminalCompletions(id: string, options: ITerminalCompletionContextDto, token: CancellationToken): Promise; } export interface ExtHostTerminalShellIntegrationShape { diff --git a/src/vs/workbench/api/common/extHostTerminalService.ts b/src/vs/workbench/api/common/extHostTerminalService.ts index 0ee84d5a8597f..907fb7125e533 100644 --- a/src/vs/workbench/api/common/extHostTerminalService.ts +++ b/src/vs/workbench/api/common/extHostTerminalService.ts @@ -56,7 +56,7 @@ export interface IExtHostTerminalService extends ExtHostTerminalServiceShape, ID getEnvironmentVariableCollection(extension: IExtensionDescription): IEnvironmentVariableCollection; getTerminalById(id: number): ExtHostTerminal | null; getTerminalIdByApiObject(apiTerminal: vscode.Terminal): number | null; - registerTerminalCompletionProvider(extension: IExtensionDescription, provider: vscode.TerminalCompletionProvider, ...triggerCharacters: string[]): vscode.Disposable; + registerTerminalCompletionProvider(extension: IExtensionDescription, provider: vscode.TerminalCompletionProvider, ...triggerCharacters: string[]): vscode.Disposable; } interface IEnvironmentVariableCollection extends vscode.EnvironmentVariableCollection { @@ -746,7 +746,7 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I }); } - public async $provideTerminalCompletions(id: string, options: ITerminalCompletionContextDto): Promise { + public async $provideTerminalCompletions(id: string, options: ITerminalCompletionContextDto): Promise { const token = new CancellationTokenSource().token; if (token.isCancellationRequested || !this.activeTerminal) { return undefined; diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index a3f704841bb49..35082a1e9e9d3 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -2145,6 +2145,41 @@ export class TerminalCompletionItem implements vscode.TerminalCompletionItem { } +/** + * Represents a collection of {@link CompletionItem completion items} to be presented + * in the editor. + */ +export class TerminalCompletionList { + + /** + * Resources should be shown in the completions list + */ + resourceRequestConfig?: TerminalResourceRequestConfig; + + /** + * The completion items. + */ + items: T[]; + + /** + * Creates a new completion list. + * + * @param items The completion items. + * @param isIncomplete The list is not complete. + */ + constructor(items?: T[], resourceRequestConfig?: TerminalResourceRequestConfig) { + this.items = items ?? []; + this.resourceRequestConfig = resourceRequestConfig; + } +} + +export interface TerminalResourceRequestConfig { + filesRequested?: boolean; + foldersRequested?: boolean; + cwd?: vscode.Uri; + pathSeparator: string; +} + export enum TaskRevealKind { Always = 1, diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts index ead61540bf2c7..4e8379c847f93 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts @@ -4,7 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import { CancellationToken } from '../../../../../base/common/cancellation.js'; import { Disposable, IDisposable, toDisposable } from '../../../../../base/common/lifecycle.js'; +import { URI } from '../../../../../base/common/uri.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; +import { IFileService } from '../../../../../platform/files/common/files.js'; import { createDecorator } from '../../../../../platform/instantiation/common/instantiation.js'; import { TerminalSettingId, TerminalShellType } from '../../../../../platform/terminal/common/terminal.js'; import { ISimpleCompletion } from '../../../../services/suggest/browser/simpleCompletionItem.js'; @@ -24,10 +26,47 @@ export interface ITerminalCompletion extends ISimpleCompletion { kind?: TerminalCompletionItemKind; } + +/** + * Represents a collection of {@link CompletionItem completion items} to be presented + * in the editor. + */ +export class TerminalCompletionList { + + /** + * Resources should be shown in the completions list + */ + resourceRequestConfig?: TerminalResourceRequestConfig; + + /** + * The completion items. + */ + items?: ITerminalCompletion[]; + + /** + * Creates a new completion list. + * + * @param items The completion items. + * @param isIncomplete The list is not complete. + */ + constructor(items?: ITerminalCompletion[], resourceRequestConfig?: TerminalResourceRequestConfig) { + this.items = items; + this.resourceRequestConfig = resourceRequestConfig; + } +} + +export interface TerminalResourceRequestConfig { + filesRequested?: boolean; + foldersRequested?: boolean; + cwd?: URI; + pathSeparator: string; +} + + export interface ITerminalCompletionProvider { id: string; shellTypes?: TerminalShellType[]; - provideCompletions(value: string, cursorPosition: number, token: CancellationToken): Promise; + provideCompletions(value: string, cursorPosition: number, token: CancellationToken): Promise | undefined>; triggerCharacters?: string[]; isBuiltin?: boolean; } @@ -55,7 +94,9 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo } } - constructor(@IConfigurationService private readonly _configurationService: IConfigurationService) { + constructor(@IConfigurationService private readonly _configurationService: IConfigurationService, + @IFileService private readonly _fileService: IFileService + ) { super(); } @@ -79,9 +120,7 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo }); } - async provideCompletions(promptValue: string, cursorPosition: number, shellType: TerminalShellType, token: CancellationToken, triggerCharacter?: boolean): Promise { - const completionItems: ISimpleCompletion[] = []; - + async provideCompletions(promptValue: string, cursorPosition: number, shellType: TerminalShellType, token: CancellationToken, triggerCharacter?: boolean): Promise { if (!this._providers || !this._providers.values) { return undefined; } @@ -110,31 +149,93 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo providers = providers.filter(p => p.isBuiltin); } - await this._collectCompletions(providers, shellType, promptValue, cursorPosition, completionItems, token); - return completionItems.length > 0 ? completionItems : undefined; + return this._collectCompletions(providers, shellType, promptValue, cursorPosition, token); } - private async _collectCompletions(providers: ITerminalCompletionProvider[], shellType: TerminalShellType, promptValue: string, cursorPosition: number, completionItems: ISimpleCompletion[], token: CancellationToken) { + private async _collectCompletions(providers: ITerminalCompletionProvider[], shellType: TerminalShellType, promptValue: string, cursorPosition: number, token: CancellationToken): Promise { const completionPromises = providers.map(async provider => { if (provider.shellTypes && !provider.shellTypes.includes(shellType)) { - return []; + return undefined; + } + const completions: ITerminalCompletion[] | TerminalCompletionList | undefined = await provider.provideCompletions(promptValue, cursorPosition, token); + if (!completions) { + return undefined; } - const completions = await provider.provideCompletions(promptValue, cursorPosition, token); const devModeEnabled = this._configurationService.getValue(TerminalSettingId.DevMode); - if (completions) { - return completions.map(completion => { - if (devModeEnabled && !completion.detail?.includes(provider.id)) { - completion.detail = `(${provider.id}) ${completion.detail ?? ''}`; - } - return completion; - }); + const completionItems = Array.isArray(completions) ? completions : completions.items ?? []; + + const itemsWithModifiedLabels = completionItems.map(completion => { + if (devModeEnabled && !completion.detail?.includes(provider.id)) { + completion.detail = `(${provider.id}) ${completion.detail ?? ''}`; + } + return completion; + }); + + if (Array.isArray(completions)) { + return itemsWithModifiedLabels; + } + if (completions.resourceRequestConfig) { + const resourceCompletions = await this._resolveResources(completions.resourceRequestConfig, promptValue, cursorPosition); + if (resourceCompletions) { + itemsWithModifiedLabels.push(...resourceCompletions); + } + return itemsWithModifiedLabels; } - return []; + return; }); const results = await Promise.all(completionPromises); - results.forEach(completions => completionItems.push(...completions)); + return results.filter(result => !!result).flat(); } -} + private async _resolveResources(resourceRequestConfig: TerminalResourceRequestConfig, promptValue: string, cursorPosition: number): Promise { + const cwd = URI.revive(resourceRequestConfig.cwd); + const foldersRequested = resourceRequestConfig.foldersRequested ?? false; + const filesRequested = resourceRequestConfig.filesRequested ?? false; + if (!cwd || (!foldersRequested && !filesRequested)) { + return; + } + + const resourceCompletions: ITerminalCompletion[] = []; + const fileStat = await this._fileService.resolve(cwd, { resolveSingleChildDescendants: true }); + + if (!fileStat || !fileStat?.children) { + return; + } + + for (const stat of fileStat.children) { + let kind: TerminalCompletionItemKind | undefined; + if (foldersRequested && stat.isDirectory) { + kind = TerminalCompletionItemKind.Folder; + } + if (filesRequested && !stat.isDirectory && (stat.isFile || stat.resource.scheme === 'file')) { + kind = TerminalCompletionItemKind.File; + } + if (kind === undefined) { + continue; + } + const lastWord = promptValue.substring(0, cursorPosition).split(' ').pop(); + const lastIndexOfDot = lastWord?.lastIndexOf('.') ?? -1; + const lastIndexOfSlash = lastWord?.lastIndexOf(resourceRequestConfig.pathSeparator) ?? -1; + let label; + if (lastIndexOfSlash > -1) { + label = stat.resource.fsPath.replace(cwd.fsPath, '').substring(1); + } else if (lastIndexOfDot === -1) { + label = '.' + stat.resource.fsPath.replace(cwd.fsPath, ''); + } else { + label = stat.resource.fsPath.replace(cwd.fsPath, ''); + } + + resourceCompletions.push({ + label, + kind, + isDirectory: kind === TerminalCompletionItemKind.Folder, + isFile: kind === TerminalCompletionItemKind.File, + replacementIndex: cursorPosition, + replacementLength: label.length + }); + } + return resourceCompletions.length ? resourceCompletions : undefined; + } +} diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts index 8eef1f305b3ad..8e9dd3c793b43 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts @@ -202,7 +202,7 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest normalizedLeadingLineContent = normalizePathSeparator(normalizedLeadingLineContent, this._pathSeparator); } for (const completion of completions) { - if (!completion.icon && completion.kind) { + if (!completion.icon && completion.kind !== undefined) { completion.icon = this._kindToIconMap.get(completion.kind); } } diff --git a/src/vscode-dts/vscode.proposed.terminalCompletionProvider.d.ts b/src/vscode-dts/vscode.proposed.terminalCompletionProvider.d.ts index 69dc907809b74..095fdcc867a54 100644 --- a/src/vscode-dts/vscode.proposed.terminalCompletionProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.terminalCompletionProvider.d.ts @@ -16,10 +16,9 @@ declare module 'vscode' { * @param token A cancellation token. * @return A list of completions. */ - provideTerminalCompletions(terminal: Terminal, context: TerminalCompletionContext, token: CancellationToken): ProviderResult; + provideTerminalCompletions(terminal: Terminal, context: TerminalCompletionContext, token: CancellationToken): ProviderResult>; } - export interface TerminalCompletionItem { /** * The label of the completion. @@ -80,4 +79,48 @@ declare module 'vscode' { */ export function registerTerminalCompletionProvider(provider: TerminalCompletionProvider, ...triggerCharacters: string[]): Disposable; } + + /** + * Represents a collection of {@link TerminalCompletionItem completion items} to be presented + * in the terminal. + */ + export class TerminalCompletionList { + + /** + * Resources that should be shown in the completions list for the cwd of the terminal. + */ + resourceRequestConfig?: TerminalResourceRequestConfig; + + /** + * The completion items. + */ + items: T[]; + + /** + * Creates a new completion list. + * + * @param items The completion items. + * @param resourceRequestConfig Indicates which resources should be shown as completions for the cwd of the terminal. + */ + constructor(items?: T[], resourceRequestConfig?: TerminalResourceRequestConfig); + } + + export interface TerminalResourceRequestConfig { + /** + * Show files as completion items. + */ + filesRequested?: boolean; + /** + * Show folders as completion items. + */ + foldersRequested?: boolean; + /** + * If no cwd is provided, no resources will be shown as completions. + */ + cwd?: Uri; + /** + * The path separator to use when constructing paths. + */ + pathSeparator: string; + } } From 5a27ba9d7d12a2c8dae952e2a04dc560ad5a8c2a Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Wed, 20 Nov 2024 16:41:27 -0800 Subject: [PATCH 022/119] testing: vertically center test message badge Closes #234287 --- .../contrib/testing/browser/testingDecorations.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/vs/workbench/contrib/testing/browser/testingDecorations.ts b/src/vs/workbench/contrib/testing/browser/testingDecorations.ts index 0a27e674ec5ce..6ee67f4bcae72 100644 --- a/src/vs/workbench/contrib/testing/browser/testingDecorations.ts +++ b/src/vs/workbench/contrib/testing/browser/testingDecorations.ts @@ -1332,6 +1332,8 @@ class TestMessageDecoration implements ITestDecoration { } } +const ERROR_CONTENT_WIDGET_HEIGHT = 20; + class TestErrorContentWidget extends Disposable implements IContentWidget { private readonly id = generateUuid(); @@ -1360,6 +1362,18 @@ class TestErrorContentWidget extends Disposable implements IContentWidget { ) { super(); + const setMarginTop = () => { + const lineHeight = editor.getOption(EditorOption.lineHeight); + this.node.root.style.marginTop = (lineHeight - ERROR_CONTENT_WIDGET_HEIGHT) / 2 + 'px'; + }; + + setMarginTop(); + this._register(editor.onDidChangeConfiguration(e => { + if (e.hasChanged(EditorOption.lineHeight)) { + setMarginTop(); + } + })); + let text: string; if (message.expected !== undefined && message.actual !== undefined) { text = `${truncateMiddle(message.actual, 15)} != ${truncateMiddle(message.expected, 15)}`; From 69fd4cf8bbd237e75afa4f2b134a44d91ee816a9 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Wed, 20 Nov 2024 17:03:54 -0800 Subject: [PATCH 023/119] testing: track test error message decorations correctly Fixes #234293 --- .../testing/browser/testingDecorations.ts | 53 +++++++++++++------ 1 file changed, 36 insertions(+), 17 deletions(-) diff --git a/src/vs/workbench/contrib/testing/browser/testingDecorations.ts b/src/vs/workbench/contrib/testing/browser/testingDecorations.ts index 6ee67f4bcae72..494f0703bbed2 100644 --- a/src/vs/workbench/contrib/testing/browser/testingDecorations.ts +++ b/src/vs/workbench/contrib/testing/browser/testingDecorations.ts @@ -20,7 +20,7 @@ import { ResourceMap } from '../../../../base/common/map.js'; import { clamp } from '../../../../base/common/numbers.js'; import { autorun } from '../../../../base/common/observable.js'; import { isMacintosh } from '../../../../base/common/platform.js'; -import { truncateMiddle } from '../../../../base/common/strings.js'; +import { count, truncateMiddle } from '../../../../base/common/strings.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; import { Constants } from '../../../../base/common/uint.js'; import { URI } from '../../../../base/common/uri.js'; @@ -360,7 +360,7 @@ export class TestingDecorations extends Disposable implements IEditorContributio /** * Results invalidated by editor changes. */ - private static invalidatedTests = new WeakSet(); + public static invalidatedTests = new WeakSet(); /** * Gets the decorations associated with the given code editor. @@ -455,20 +455,18 @@ export class TestingDecorations extends Disposable implements IEditorContributio } let changed = false; - for (const decos of [this.errorContentWidgets, this.loggedMessageDecorations]) { - for (const [message, deco] of decos) { - // invalidate decorations if either the line they're on was changed, - // or if the range of the test was changed. The range of the test is - // not always present, so check bo. - const invalidate = evts.some(e => e.changes.some(c => - c.range.startLineNumber <= deco.line && c.range.endLineNumber >= deco.line - || (deco.resultItem?.item.range && deco.resultItem.item.range.startLineNumber <= c.range.startLineNumber && deco.resultItem.item.range.endLineNumber >= c.range.endLineNumber) - )); - - if (invalidate) { - changed = true; - TestingDecorations.invalidatedTests.add(deco.resultItem || message); - } + for (const [message, deco] of this.loggedMessageDecorations) { + // invalidate decorations if either the line they're on was changed, + // or if the range of the test was changed. The range of the test is + // not always present, so check bo. + const invalidate = evts.some(e => e.changes.some(c => + c.range.startLineNumber <= deco.line && c.range.endLineNumber >= deco.line + || (deco.resultItem?.item.range && deco.resultItem.item.range.startLineNumber <= c.range.startLineNumber && deco.resultItem.item.range.endLineNumber >= c.range.endLineNumber) + )); + + if (invalidate) { + changed = true; + TestingDecorations.invalidatedTests.add(deco.resultItem || message); } } @@ -1354,7 +1352,7 @@ class TestErrorContentWidget extends Disposable implements IContentWidget { constructor( private readonly editor: ICodeEditor, - private readonly position: Position, + private position: Position, public readonly message: ITestErrorMessage, public readonly resultItem: TestResultItem, uri: URI, @@ -1411,6 +1409,27 @@ class TestErrorContentWidget extends Disposable implements IContentWidget { this.node.arrow.appendChild(svg); + this._register(editor.onDidChangeModelContent(e => { + for (const c of e.changes) { + if (c.range.startLineNumber > this.line) { + continue; + } + if ( + c.range.startLineNumber <= this.line && c.range.endLineNumber >= this.line + || (resultItem.item.range && resultItem.item.range.startLineNumber <= c.range.startLineNumber && resultItem.item.range.endLineNumber >= c.range.endLineNumber) + ) { + TestingDecorations.invalidatedTests.add(this.resultItem); + this.dispose(); // todo + } + + const adjust = count(c.text, '\n') - (c.range.endLineNumber - c.range.startLineNumber); + if (adjust !== 0) { + this.position = this.position.delta(adjust); + this.editor.layoutContentWidget(this); + } + } + })); + editor.addContentWidget(this); this._register(toDisposable(() => editor.removeContentWidget(this))); } From 26ff4385ad7653b4c3327bc052cc47ceaa8e4117 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 21 Nov 2024 08:25:40 +0100 Subject: [PATCH 024/119] chat - tweaks to welcome (#234250) --- src/vs/base/common/product.ts | 41 ++- .../chat/browser/actions/chatActions.ts | 19 +- .../browser/actions/chatGettingStarted.ts | 12 +- .../browser/chatParticipantContributions.ts | 20 +- .../chat/browser/chatSetup.contribution.ts | 238 +++++++++++------- .../chat/browser/media/chatViewWelcome.css | 4 + .../viewsWelcome/chatViewWelcomeController.ts | 2 +- .../browser/viewsWelcome/chatViewsWelcome.ts | 2 +- .../contrib/chat/common/chatContextKeys.ts | 9 +- .../browser/gettingStartedService.ts | 2 +- 10 files changed, 204 insertions(+), 145 deletions(-) diff --git a/src/vs/base/common/product.ts b/src/vs/base/common/product.ts index 9c4f060e7c878..3cf230772d624 100644 --- a/src/vs/base/common/product.ts +++ b/src/vs/base/common/product.ts @@ -191,22 +191,11 @@ export interface IProductConfiguration { readonly commonlyUsedSettings?: string[]; readonly aiGeneratedWorkspaceTrust?: IAiGeneratedWorkspaceTrust; - readonly gitHubEntitlement?: IGitHubEntitlement; + + readonly defaultChatAgent?: IDefaultChatAgent; readonly chatParticipantRegistry?: string; readonly emergencyAlertUrl?: string; - - readonly defaultChatAgent?: { - readonly extensionId: string; - readonly providerId: string; - readonly providerName: string; - readonly providerScopes: string[]; - readonly name: string; - readonly icon: string; - readonly documentationUrl: string; - readonly gettingStartedCommand: string; - readonly welcomeTitle: string; - }; } export interface ITunnelApplicationConfig { @@ -312,14 +301,20 @@ export interface IAiGeneratedWorkspaceTrust { readonly startupTrustRequestLearnMore: string; } -export interface IGitHubEntitlement { - providerId: string; - command: { title: string; titleWithoutPlaceHolder: string; action: string; when: string }; - entitlementUrl: string; - extensionId: string; - enablementKey: string; - trialKey: string; - trialValue: string; - confirmationMessage: string; - confirmationAction: string; +export interface IDefaultChatAgent { + readonly extensionId: string; + readonly name: string; + readonly icon: string; + readonly chatExtensionId: string; + readonly chatName: string; + readonly chatWelcomeTitle: string; + readonly documentationUrl: string; + readonly privacyStatementUrl: string; + readonly providerId: string; + readonly providerName: string; + readonly providerScopes: string[]; + readonly entitlementUrl: string; + readonly entitlementChatEnabled: string; + readonly entitlementSkuKey: string; + readonly entitlementSku30DTrialValue: string; } diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts index a66b9a645dbac..59c8caf43aea9 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts @@ -88,7 +88,10 @@ class OpenChatGlobalAction extends Action2 { title: OpenChatGlobalAction.TITLE, icon: defaultChat.icon, f1: true, - precondition: ChatContextKeys.panelParticipantRegistered, + precondition: ContextKeyExpr.or( + ChatContextKeys.Setup.installed, + ChatContextKeys.panelParticipantRegistered + ), category: CHAT_CATEGORY, keybinding: { weight: KeybindingWeight.WorkbenchContrib, @@ -508,9 +511,10 @@ MenuRegistry.appendMenuItem(MenuId.CommandCenter, { when: ContextKeyExpr.and( ContextKeyExpr.has('config.chat.commandCenter.enabled'), ContextKeyExpr.or( - ChatContextKeys.panelParticipantRegistered, - ChatContextKeys.ChatSetup.entitled, - ContextKeyExpr.has('config.chat.experimental.offerSetup') + ChatContextKeys.Setup.installed, + ChatContextKeys.Setup.entitled, + ContextKeyExpr.has('config.chat.experimental.offerSetup'), + ChatContextKeys.panelParticipantRegistered ) ), order: 10001, @@ -525,9 +529,10 @@ registerAction2(class ToggleChatControl extends ToggleTitleBarConfigAction { ContextKeyExpr.and( ContextKeyExpr.has('config.window.commandCenter'), ContextKeyExpr.or( - ChatContextKeys.panelParticipantRegistered, - ChatContextKeys.ChatSetup.entitled, - ContextKeyExpr.has('config.chat.experimental.offerSetup') + ChatContextKeys.Setup.installed, + ChatContextKeys.Setup.entitled, + ContextKeyExpr.has('config.chat.experimental.offerSetup'), + ChatContextKeys.panelParticipantRegistered ) ) ); diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatGettingStarted.ts b/src/vs/workbench/contrib/chat/browser/actions/chatGettingStarted.ts index 34cd4aa113ece..df44df5044704 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatGettingStarted.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatGettingStarted.ts @@ -12,6 +12,7 @@ import { ExtensionIdentifier } from '../../../../../platform/extensions/common/e import { CHAT_OPEN_ACTION_ID } from './chatActions.js'; import { IExtensionManagementService, InstallOperation } from '../../../../../platform/extensionManagement/common/extensionManagement.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js'; +import { IDefaultChatAgent } from '../../../../../base/common/product.js'; export class ChatGettingStartedContribution extends Disposable implements IWorkbenchContribution { @@ -29,19 +30,20 @@ export class ChatGettingStartedContribution extends Disposable implements IWorkb ) { super(); + const defaultChatAgent = this.productService.defaultChatAgent; const hideWelcomeView = this.storageService.getBoolean(ChatGettingStartedContribution.hideWelcomeView, StorageScope.APPLICATION, false); - if (!this.productService.gitHubEntitlement || hideWelcomeView) { + if (!defaultChatAgent || hideWelcomeView) { return; } - this.registerListeners(); + this.registerListeners(defaultChatAgent); } - private registerListeners() { + private registerListeners(defaultChatAgent: IDefaultChatAgent): void { this._register(this.extensionManagementService.onDidInstallExtensions(async (result) => { for (const e of result) { - if (ExtensionIdentifier.equals(this.productService.gitHubEntitlement!.extensionId, e.identifier.id) && e.operation === InstallOperation.Install) { + if (ExtensionIdentifier.equals(defaultChatAgent.extensionId, e.identifier.id) && e.operation === InstallOperation.Install) { this.recentlyInstalled = true; return; } @@ -50,7 +52,7 @@ export class ChatGettingStartedContribution extends Disposable implements IWorkb this._register(this.extensionService.onDidChangeExtensionsStatus(async (event) => { for (const ext of event) { - if (ExtensionIdentifier.equals(this.productService.gitHubEntitlement!.extensionId, ext.value)) { + if (ExtensionIdentifier.equals(defaultChatAgent.extensionId, ext.value)) { const extensionStatus = this.extensionService.getExtensionsStatus(); if (extensionStatus[ext.value].activationTimes && this.recentlyInstalled) { await this.commandService.executeCommand(CHAT_OPEN_ACTION_ID); diff --git a/src/vs/workbench/contrib/chat/browser/chatParticipantContributions.ts b/src/vs/workbench/contrib/chat/browser/chatParticipantContributions.ts index 5809333e2cda3..9eefdcd1709bb 100644 --- a/src/vs/workbench/contrib/chat/browser/chatParticipantContributions.ts +++ b/src/vs/workbench/contrib/chat/browser/chatParticipantContributions.ts @@ -320,9 +320,12 @@ export class ChatExtensionPointHandler implements IWorkbenchContribution { }, ctorDescriptor: new SyncDescriptor(ChatViewPane, [{ location: ChatAgentLocation.Panel }]), when: ContextKeyExpr.or( + ChatContextKeys.Setup.triggered, + ChatContextKeys.Setup.signingIn, + ChatContextKeys.Setup.installing, + ChatContextKeys.Setup.installed, ChatContextKeys.panelParticipantRegistered, - ChatContextKeys.extensionInvalid, - ChatContextKeys.setupRunning + ChatContextKeys.extensionInvalid ) }]; Registry.as(ViewExtensions.ViewsRegistry).registerViews(viewDescriptor, this._viewContainer); @@ -368,7 +371,10 @@ export class ChatExtensionPointHandler implements IWorkbenchContribution { order: 2 }, ctorDescriptor: new SyncDescriptor(ChatViewPane, [{ location: ChatAgentLocation.EditingSession }]), - when: ChatContextKeys.editingParticipantRegistered + when: ContextKeyExpr.or( + ChatContextKeys.Setup.installed, + ChatContextKeys.editingParticipantRegistered + ) }]; Registry.as(ViewExtensions.ViewsRegistry).registerViews(viewDescriptor, viewContainer); @@ -402,7 +408,7 @@ export class ChatCompatibilityNotifier extends Disposable implements IWorkbenchC extensionsWorkbenchService.onDidChangeExtensionsNotification, () => { const notification = extensionsWorkbenchService.getExtensionsNotification(); - const chatExtension = notification?.extensions.find(ext => ext.identifier.id === 'github.copilot-chat'); + const chatExtension = notification?.extensions.find(ext => ExtensionIdentifier.equals(ext.identifier.id, this.productService.defaultChatAgent?.chatExtensionId)); if (chatExtension) { isInvalid.set(true); this.registerWelcomeView(chatExtension); @@ -420,9 +426,9 @@ export class ChatCompatibilityNotifier extends Disposable implements IWorkbenchC this.registeredWelcomeView = true; const showExtensionLabel = localize('showExtension', "Show Extension"); - const mainMessage = localize('chatFailErrorMessage', "Chat failed to load because the installed version of the {0} extension is not compatible with this version of {1}. Please ensure that the GitHub Copilot Chat extension is up to date.", 'GitHub Copilot Chat', this.productService.nameLong); - const commandButton = `[${showExtensionLabel}](command:${showExtensionsWithIdsCommandId}?${encodeURIComponent(JSON.stringify([['GitHub.copilot-chat']]))})`; - const versionMessage = `GitHub Copilot Chat version: ${chatExtension.version}`; + const mainMessage = localize('chatFailErrorMessage', "Chat failed to load because the installed version of the {0} extension is not compatible with this version of {1}. Please ensure that the {2} extension is up to date.", this.productService.defaultChatAgent?.chatName, this.productService.nameLong, this.productService.defaultChatAgent?.chatName); + const commandButton = `[${showExtensionLabel}](command:${showExtensionsWithIdsCommandId}?${encodeURIComponent(JSON.stringify([[this.productService.defaultChatAgent?.chatExtensionId]]))})`; + const versionMessage = `${this.productService.defaultChatAgent?.chatName} version: ${chatExtension.version}`; const viewsRegistry = Registry.as(ViewExtensions.ViewsRegistry); this._register(viewsRegistry.registerViewWelcomeContent(ChatViewId, { content: [mainMessage, commandButton, versionMessage].join('\n\n'), diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts b/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts index da472b89dd557..affafca838a8c 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts @@ -13,11 +13,10 @@ import { IExtensionManagementService } from '../../../../platform/extensionManag import { ExtensionIdentifier } from '../../../../platform/extensions/common/extensions.js'; import { IExtensionService } from '../../../services/extensions/common/extensions.js'; import { IRequestService, asText } from '../../../../platform/request/common/request.js'; -import { CancellationTokenSource } from '../../../../base/common/cancellation.js'; +import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js'; import { ChatContextKeys } from '../common/chatContextKeys.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { IRequestContext } from '../../../../base/parts/request/common/request.js'; -import { IGitHubEntitlement } from '../../../../base/common/product.js'; import { timeout } from '../../../../base/common/async.js'; import { isCancellationError } from '../../../../base/common/errors.js'; import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js'; @@ -39,17 +38,22 @@ import { MarkdownString } from '../../../../base/common/htmlContent.js'; import { IViewDescriptorService, ViewContainerLocation } from '../../../common/views.js'; import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; import { getActiveElement } from '../../../../base/browser/dom.js'; +import { ILogService } from '../../../../platform/log/common/log.js'; const defaultChat = { extensionId: product.defaultChatAgent?.extensionId ?? '', name: product.defaultChatAgent?.name ?? '', + icon: Codicon[product.defaultChatAgent?.icon as keyof typeof Codicon ?? 'commentDiscussion'], + chatWelcomeTitle: product.defaultChatAgent?.chatWelcomeTitle ?? '', + documentationUrl: product.defaultChatAgent?.documentationUrl ?? '', + privacyStatementUrl: product.defaultChatAgent?.privacyStatementUrl ?? '', providerId: product.defaultChatAgent?.providerId ?? '', providerName: product.defaultChatAgent?.providerName ?? '', providerScopes: product.defaultChatAgent?.providerScopes ?? [], - icon: Codicon[product.defaultChatAgent?.icon as keyof typeof Codicon ?? 'commentDiscussion'], - documentationUrl: product.defaultChatAgent?.documentationUrl ?? '', - gettingStartedCommand: product.defaultChatAgent?.gettingStartedCommand ?? '', - welcomeTitle: product.defaultChatAgent?.welcomeTitle ?? '', + entitlementUrl: product.defaultChatAgent?.entitlementUrl ?? '', + entitlementSkuKey: product.defaultChatAgent?.entitlementSkuKey ?? '', + entitlementSku30DTrialValue: product.defaultChatAgent?.entitlementSku30DTrialValue ?? '', + entitlementChatEnabled: product.defaultChatAgent?.entitlementChatEnabled ?? '', }; type ChatSetupEntitlementEnablementClassification = { @@ -75,14 +79,21 @@ type InstallChatEvent = { signedIn: boolean; }; +interface IChatEntitlement { + readonly chatEnabled?: boolean; + readonly chatSku30DTrial?: boolean; +} + +const UNKNOWN_CHAT_ENTITLEMENT: IChatEntitlement = {}; + class ChatSetupContribution extends Disposable implements IWorkbenchContribution { - private readonly chatSetupSignedInContextKey = ChatContextKeys.ChatSetup.signedIn.bindTo(this.contextKeyService); - private readonly chatSetupEntitledContextKey = ChatContextKeys.ChatSetup.entitled.bindTo(this.contextKeyService); + private readonly chatSetupSignedInContextKey = ChatContextKeys.Setup.signedIn.bindTo(this.contextKeyService); + private readonly chatSetupEntitledContextKey = ChatContextKeys.Setup.entitled.bindTo(this.contextKeyService); private readonly chatSetupState = this.instantiationService.createInstance(ChatSetupState); - private resolvedEntitlement: boolean | undefined = undefined; + private resolvedEntitlement: IChatEntitlement | undefined = undefined; constructor( @IContextKeyService private readonly contextKeyService: IContextKeyService, @@ -91,90 +102,105 @@ class ChatSetupContribution extends Disposable implements IWorkbenchContribution @IProductService private readonly productService: IProductService, @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, @IExtensionService private readonly extensionService: IExtensionService, - @IRequestService private readonly requestService: IRequestService, @IInstantiationService private readonly instantiationService: IInstantiationService ) { super(); - const entitlement = this.productService.gitHubEntitlement; - if (!entitlement) { + if (!this.productService.defaultChatAgent) { return; } this.registerChatWelcome(); - this.registerEntitlementListeners(entitlement); - this.registerAuthListeners(entitlement); + this.registerEntitlementListeners(); + this.registerAuthListeners(); - this.checkExtensionInstallation(entitlement); + this.checkExtensionInstallation(); } private registerChatWelcome(): void { + const header = localize('setupPreamble1', "{0} is your AI pair programmer that helps you write code faster and smarter.", defaultChat.name); + const footer = localize('setupPreamble2', "By proceeding you agree to the [Privacy Statement]({0}).", defaultChat.privacyStatementUrl); // Setup: Triggered (signed-out) Registry.as(ChatViewsWelcomeExtensions.ChatViewsWelcomeRegistry).register({ - title: defaultChat.welcomeTitle, + title: defaultChat.chatWelcomeTitle, when: ContextKeyExpr.and( - ChatContextKeys.ChatSetup.triggered, - ChatContextKeys.ChatSetup.signedIn.negate(), - ChatContextKeys.ChatSetup.signingIn.negate(), - ChatContextKeys.ChatSetup.installing.negate(), - ChatContextKeys.extensionInvalid.negate(), - ChatContextKeys.panelParticipantRegistered.negate() + ChatContextKeys.Setup.triggered, + ChatContextKeys.Setup.signedIn.negate(), + ChatContextKeys.Setup.signingIn.negate(), + ChatContextKeys.Setup.installing.negate(), + ChatContextKeys.Setup.installed.negate() )!, icon: defaultChat.icon, - content: new MarkdownString(`${localize('setupContent', "{0} is your AI pair programmer that helps you write code faster and smarter.", defaultChat.name)}\n\n[${localize('signInAndSetup', "Sign in to use {0}", defaultChat.name)}](command:${ChatSetupSignInAndInstallChatAction.ID})\n\n[${localize('learnMore', "Learn More")}](${defaultChat.documentationUrl}) | [${localize('hideSetup', "Hide")}](command:${ChatSetupHideAction.ID} "${localize('hideSetup', "Hide")}")`, { isTrusted: true }), + content: new MarkdownString([ + header, + `[${localize('signInAndSetup', "Sign in to use {0}", defaultChat.name)}](command:${ChatSetupSignInAndInstallChatAction.ID})`, + footer, + `[${localize('learnMore', "Learn More")}](${defaultChat.documentationUrl}) | [${localize('hideSetup', "Hide")}](command:${ChatSetupHideAction.ID} "${localize('hideSetup', "Hide")}")`, + ].join('\n\n'), { isTrusted: true }), }); // Setup: Triggered (signed-in) Registry.as(ChatViewsWelcomeExtensions.ChatViewsWelcomeRegistry).register({ - title: defaultChat.welcomeTitle, + title: defaultChat.chatWelcomeTitle, when: ContextKeyExpr.and( - ChatContextKeys.ChatSetup.triggered, - ChatContextKeys.ChatSetup.signedIn, - ChatContextKeys.ChatSetup.signingIn.negate(), - ChatContextKeys.ChatSetup.installing.negate(), - ChatContextKeys.extensionInvalid.negate(), - ChatContextKeys.panelParticipantRegistered.negate() + ChatContextKeys.Setup.triggered, + ChatContextKeys.Setup.signedIn, + ChatContextKeys.Setup.signingIn.negate(), + ChatContextKeys.Setup.installing.negate(), + ChatContextKeys.Setup.installed.negate() )!, icon: defaultChat.icon, - content: new MarkdownString(`${localize('setupContent', "{0} is your AI pair programmer that helps you write code faster and smarter.", defaultChat.name)}\n\n[${localize('setup', "Install {0}", defaultChat.name)}](command:${ChatSetupInstallAction.ID})\n\n[${localize('learnMore', "Learn More")}](${defaultChat.documentationUrl}) | [${localize('hideSetup', "Hide")}](command:${ChatSetupHideAction.ID} "${localize('hideSetup', "Hide")}")`, { isTrusted: true }), + content: new MarkdownString([ + header, + `[${localize('setup', "Install {0}", defaultChat.name)}](command:${ChatSetupInstallAction.ID})`, + footer, + `[${localize('learnMore', "Learn More")}](${defaultChat.documentationUrl}) | [${localize('hideSetup', "Hide")}](command:${ChatSetupHideAction.ID} "${localize('hideSetup', "Hide")}")`, + ].join('\n\n'), { isTrusted: true }) }); // Setup: Signing-in Registry.as(ChatViewsWelcomeExtensions.ChatViewsWelcomeRegistry).register({ - title: defaultChat.welcomeTitle, + title: defaultChat.chatWelcomeTitle, when: ContextKeyExpr.and( - ChatContextKeys.ChatSetup.signingIn, - ChatContextKeys.extensionInvalid.negate(), - ChatContextKeys.panelParticipantRegistered.negate() + ChatContextKeys.Setup.signingIn, + ChatContextKeys.Setup.installed.negate() )!, icon: defaultChat.icon, progress: localize('setupChatSigningIn', "Signing in to {0}...", defaultChat.providerName), - content: new MarkdownString(`${localize('setupContent', "{0} is your AI pair programmer that helps you write code faster and smarter.", defaultChat.name)}`, { isTrusted: true }), + content: new MarkdownString([ + header, + footer, + `[${localize('learnMore', "Learn More")}](${defaultChat.documentationUrl})`, + ].join('\n\n'), { isTrusted: true }), }); // Setup: Installing Registry.as(ChatViewsWelcomeExtensions.ChatViewsWelcomeRegistry).register({ - title: defaultChat.welcomeTitle, - when: ChatContextKeys.ChatSetup.installing, + title: defaultChat.chatWelcomeTitle, + when: ChatContextKeys.Setup.installing, icon: defaultChat.icon, progress: localize('setupChatInstalling', "Setting up Chat for you..."), - content: new MarkdownString(`${localize('setupContent', "{0} is your AI pair programmer that helps you write code faster and smarter.", defaultChat.name)}`, { isTrusted: true }), + content: new MarkdownString([ + header, + footer, + `[${localize('learnMore', "Learn More")}](${defaultChat.documentationUrl})`, + ].join('\n\n'), { isTrusted: true }), }); } - private registerEntitlementListeners(entitlement: IGitHubEntitlement): void { + private registerEntitlementListeners(): void { this._register(this.extensionService.onDidChangeExtensions(result => { for (const extension of result.removed) { - if (ExtensionIdentifier.equals(entitlement.extensionId, extension.identifier)) { + if (ExtensionIdentifier.equals(defaultChat.extensionId, extension.identifier)) { this.chatSetupState.update({ chatInstalled: false }); break; } } for (const extension of result.added) { - if (ExtensionIdentifier.equals(entitlement.extensionId, extension.identifier)) { + if (ExtensionIdentifier.equals(defaultChat.extensionId, extension.identifier)) { this.chatSetupState.update({ chatInstalled: true }); break; } @@ -182,9 +208,9 @@ class ChatSetupContribution extends Disposable implements IWorkbenchContribution })); this._register(this.authenticationService.onDidChangeSessions(e => { - if (e.providerId === entitlement.providerId) { + if (e.providerId === defaultChat.providerId) { if (e.event.added?.length) { - this.resolveEntitlement(entitlement, e.event.added[0]); + this.resolveEntitlement(e.event.added[0]); } else if (e.event.removed?.length) { this.chatSetupEntitledContextKey.set(false); } @@ -192,20 +218,20 @@ class ChatSetupContribution extends Disposable implements IWorkbenchContribution })); this._register(this.authenticationService.onDidRegisterAuthenticationProvider(async e => { - if (e.id === entitlement.providerId) { - this.resolveEntitlement(entitlement, (await this.authenticationService.getSessions(e.id))[0]); + if (e.id === defaultChat.providerId) { + this.resolveEntitlement((await this.authenticationService.getSessions(e.id))[0]); } })); } - private registerAuthListeners(entitlement: IGitHubEntitlement): void { + private registerAuthListeners(): void { const hasProviderSessions = async () => { - const sessions = await this.authenticationService.getSessions(entitlement.providerId); + const sessions = await this.authenticationService.getSessions(defaultChat.providerId); return sessions.length > 0; }; const handleDeclaredAuthProviders = async () => { - if (this.authenticationService.declaredProviders.find(p => p.id === entitlement.providerId)) { + if (this.authenticationService.declaredProviders.find(p => p.id === defaultChat.providerId)) { this.chatSetupSignedInContextKey.set(await hasProviderSessions()); } }; @@ -215,82 +241,109 @@ class ChatSetupContribution extends Disposable implements IWorkbenchContribution handleDeclaredAuthProviders(); this._register(this.authenticationService.onDidChangeSessions(async ({ providerId }) => { - if (providerId === entitlement.providerId) { + if (providerId === defaultChat.providerId) { this.chatSetupSignedInContextKey.set(await hasProviderSessions()); } })); } - private async resolveEntitlement(entitlement: IGitHubEntitlement, session: AuthenticationSession | undefined): Promise { + private async resolveEntitlement(session: AuthenticationSession | undefined): Promise { if (!session) { return; } - const entitled = await this.doResolveEntitlement(entitlement, session); - this.chatSetupEntitledContextKey.set(entitled); + const entitlement = await this.doResolveEntitlement(session); + this.chatSetupEntitledContextKey.set(!!entitlement.chatEnabled); } - private async doResolveEntitlement(entitlement: IGitHubEntitlement, session: AuthenticationSession): Promise { - if (typeof this.resolvedEntitlement === 'boolean') { + private async doResolveEntitlement(session: AuthenticationSession): Promise { + if (this.resolvedEntitlement) { return this.resolvedEntitlement; } const cts = new CancellationTokenSource(); this._register(toDisposable(() => cts.dispose(true))); - let context: IRequestContext; - try { - context = await this.requestService.request({ - type: 'GET', - url: entitlement.entitlementUrl, - headers: { - 'Authorization': `Bearer ${session.accessToken}` - } - }, cts.token); - } catch (error) { - return false; + const context = await this.instantiationService.invokeFunction(accessor => ChatSetupRequestHelper.request(accessor, defaultChat.entitlementUrl, 'GET', session, cts.token)); + if (!context) { + return UNKNOWN_CHAT_ENTITLEMENT; } if (context.res.statusCode && context.res.statusCode !== 200) { - return false; + return UNKNOWN_CHAT_ENTITLEMENT; } const result = await asText(context); if (!result) { - return false; + return UNKNOWN_CHAT_ENTITLEMENT; } let parsedResult: any; try { parsedResult = JSON.parse(result); } catch (err) { - return false; //ignore + return UNKNOWN_CHAT_ENTITLEMENT; } - this.resolvedEntitlement = Boolean(parsedResult[entitlement.enablementKey]); - const trial = parsedResult[entitlement.trialKey] === entitlement.trialValue; + this.resolvedEntitlement = { + chatEnabled: Boolean(parsedResult[defaultChat.entitlementChatEnabled]), + chatSku30DTrial: parsedResult[defaultChat.entitlementSkuKey] === defaultChat.entitlementSku30DTrialValue + }; + this.telemetryService.publicLog2('chatInstallEntitlement', { - entitled: this.resolvedEntitlement, - trial + entitled: !!this.resolvedEntitlement.chatEnabled, + trial: !!this.resolvedEntitlement.chatSku30DTrial }); return this.resolvedEntitlement; } - private async checkExtensionInstallation(entitlement: IGitHubEntitlement): Promise { + private async checkExtensionInstallation(): Promise { const extensions = await this.extensionManagementService.getInstalled(); - const chatInstalled = !!extensions.find(value => ExtensionIdentifier.equals(value.identifier.id, entitlement.extensionId)); + const chatInstalled = !!extensions.find(value => ExtensionIdentifier.equals(value.identifier.id, defaultChat.extensionId)); this.chatSetupState.update({ chatInstalled }); } } +class ChatSetupRequestHelper { + + static async request(accessor: ServicesAccessor, url: string, type: 'GET' | 'POST', session: AuthenticationSession | undefined, token: CancellationToken): Promise { + const requestService = accessor.get(IRequestService); + const logService = accessor.get(ILogService); + const authenticationService = accessor.get(IAuthenticationService); + + try { + if (!session) { + session = (await authenticationService.getSessions(defaultChat.providerId))[0]; + } + + if (!session) { + throw new Error('ChatSetupRequestHelper: No session found for provider'); + } + + return await requestService.request({ + type, + url, + headers: { + 'Authorization': `Bearer ${session.accessToken}` + } + }, token); + } catch (error) { + logService.error(error); + + return undefined; + } + } +} + class ChatSetupState { private static readonly CHAT_SETUP_TRIGGERD = 'chat.setupTriggered'; private static readonly CHAT_EXTENSION_INSTALLED = 'chat.extensionInstalled'; - private readonly chatSetupTriggeredContext = ChatContextKeys.ChatSetup.triggered.bindTo(this.contextKeyService); + private readonly chatSetupTriggeredContext = ChatContextKeys.Setup.triggered.bindTo(this.contextKeyService); + private readonly chatSetupInstalledContext = ChatContextKeys.Setup.installed.bindTo(this.contextKeyService); constructor( @IContextKeyService private readonly contextKeyService: IContextKeyService, @@ -305,6 +358,7 @@ class ChatSetupState { update(context: { triggered?: boolean; chatInstalled?: boolean }): void { if (typeof context.chatInstalled === 'boolean') { this.storageService.store(ChatSetupState.CHAT_EXTENSION_INSTALLED, context.chatInstalled, StorageScope.PROFILE, StorageTarget.MACHINE); + this.storageService.store(ChatSetupState.CHAT_SETUP_TRIGGERD, true, StorageScope.PROFILE, StorageTarget.MACHINE); // allows to fallback to setup view if the extension is uninstalled } if (typeof context.triggered === 'boolean') { @@ -330,6 +384,7 @@ class ChatSetupState { } this.chatSetupTriggeredContext.set(showChatSetup); + this.chatSetupInstalledContext.set(chatInstalled); } } @@ -402,21 +457,18 @@ class ChatSetupInstallAction extends Action2 { group: 'a_open', order: 0, when: ContextKeyExpr.and( - ChatContextKeys.panelParticipantRegistered.negate(), - ContextKeyExpr.or( - ChatContextKeys.ChatSetup.entitled, - ChatContextKeys.ChatSetup.signedIn - ) + ChatContextKeys.Setup.signedIn, + ChatContextKeys.Setup.installed.negate() ) } }); } override run(accessor: ServicesAccessor): Promise { - return ChatSetupInstallAction.install(accessor, false); + return ChatSetupInstallAction.install(accessor, undefined); } - static async install(accessor: ServicesAccessor, signedIn: boolean) { + static async install(accessor: ServicesAccessor, session: AuthenticationSession | undefined) { const extensionsWorkbenchService = accessor.get(IExtensionsWorkbenchService); const productService = accessor.get(IProductService); const telemetryService = accessor.get(ITelemetryService); @@ -424,7 +476,8 @@ class ChatSetupInstallAction extends Action2 { const viewsService = accessor.get(IViewsService); const chatAgentService = accessor.get(IChatAgentService); - const setupInstallingContextKey = ChatContextKeys.ChatSetup.installing.bindTo(contextKeyService); + const signedIn = !!session; + const setupInstallingContextKey = ChatContextKeys.Setup.installing.bindTo(contextKeyService); const activeElement = getActiveElement(); let installResult: 'installed' | 'cancelled' | 'failedInstall'; @@ -445,7 +498,7 @@ class ChatSetupInstallAction extends Action2 { telemetryService.publicLog2('commandCenter.chatInstall', { installResult, signedIn }); - await Promise.race([timeout(2000), Event.toPromise(chatAgentService.onDidChangeAgents)]); // reduce flicker (https://github.com/microsoft/vscode-copilot/issues/9274) + await Promise.race([timeout(5000), Event.toPromise(chatAgentService.onDidChangeAgents)]); // reduce flicker (https://github.com/microsoft/vscode-copilot/issues/9274) setupInstallingContextKey.reset(); @@ -470,9 +523,8 @@ class ChatSetupSignInAndInstallChatAction extends Action2 { group: 'a_open', order: 0, when: ContextKeyExpr.and( - ChatContextKeys.panelParticipantRegistered.negate(), - ChatContextKeys.ChatSetup.entitled.negate(), - ChatContextKeys.ChatSetup.signedIn.negate() + ChatContextKeys.Setup.signedIn.negate(), + ChatContextKeys.Setup.installed.negate() ) } }); @@ -484,11 +536,8 @@ class ChatSetupSignInAndInstallChatAction extends Action2 { const telemetryService = accessor.get(ITelemetryService); const contextKeyService = accessor.get(IContextKeyService); const viewsService = accessor.get(IViewsService); - const layoutService = accessor.get(IWorkbenchLayoutService); - const hideSecondarySidebar = !layoutService.isVisible(Parts.AUXILIARYBAR_PART); - - const setupSigningInContextKey = ChatContextKeys.ChatSetup.signingIn.bindTo(contextKeyService); + const setupSigningInContextKey = ChatContextKeys.Setup.signingIn.bindTo(contextKeyService); let session: AuthenticationSession | undefined; try { @@ -502,11 +551,8 @@ class ChatSetupSignInAndInstallChatAction extends Action2 { } if (session) { - instantiationService.invokeFunction(accessor => ChatSetupInstallAction.install(accessor, true)); + instantiationService.invokeFunction(accessor => ChatSetupInstallAction.install(accessor, session)); } else { - if (hideSecondarySidebar) { - layoutService.setPartHidden(true, Parts.AUXILIARYBAR_PART); - } telemetryService.publicLog2('commandCenter.chatInstall', { installResult: 'failedNotSignedIn', signedIn: false }); } } diff --git a/src/vs/workbench/contrib/chat/browser/media/chatViewWelcome.css b/src/vs/workbench/contrib/chat/browser/media/chatViewWelcome.css index 59c014d6b5f36..4e54182995cc0 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chatViewWelcome.css +++ b/src/vs/workbench/contrib/chat/browser/media/chatViewWelcome.css @@ -35,6 +35,10 @@ div.chat-welcome-view { flex-direction: column; align-items: center; + & > .chat-welcome-view-icon { + min-height: 48px; + } + & > .chat-welcome-view-icon .codicon { font-size: 40px; } diff --git a/src/vs/workbench/contrib/chat/browser/viewsWelcome/chatViewWelcomeController.ts b/src/vs/workbench/contrib/chat/browser/viewsWelcome/chatViewWelcomeController.ts index 81dd0149a7617..09cbdccb926f2 100644 --- a/src/vs/workbench/contrib/chat/browser/viewsWelcome/chatViewWelcomeController.ts +++ b/src/vs/workbench/contrib/chat/browser/viewsWelcome/chatViewWelcomeController.ts @@ -144,7 +144,7 @@ export class ChatViewWelcomePart extends Disposable { title.textContent = content.title; const renderer = this.instantiationService.createInstance(MarkdownRenderer, {}); const messageResult = this._register(renderer.render(content.message)); - const firstLink = options?.firstLinkToButton ? messageResult.element.querySelector('a') : undefined; + const firstLink = (options?.firstLinkToButton && !content.progress) ? messageResult.element.querySelector('a') : undefined; if (firstLink) { const target = firstLink.getAttribute('data-href'); const button = this._register(new Button(firstLink.parentElement!, defaultButtonStyles)); diff --git a/src/vs/workbench/contrib/chat/browser/viewsWelcome/chatViewsWelcome.ts b/src/vs/workbench/contrib/chat/browser/viewsWelcome/chatViewsWelcome.ts index 9980a78c5dd0a..d522a1029d2bc 100644 --- a/src/vs/workbench/contrib/chat/browser/viewsWelcome/chatViewsWelcome.ts +++ b/src/vs/workbench/contrib/chat/browser/viewsWelcome/chatViewsWelcome.ts @@ -17,7 +17,7 @@ export interface IChatViewsWelcomeDescriptor { icon?: ThemeIcon; title: string; content: IMarkdownString; - progress?: string; // TODO@bpasero remove me if not used anymore + progress?: string; when: ContextKeyExpression; } diff --git a/src/vs/workbench/contrib/chat/common/chatContextKeys.ts b/src/vs/workbench/contrib/chat/common/chatContextKeys.ts index cca651b36c57b..d5a31e868e169 100644 --- a/src/vs/workbench/contrib/chat/common/chatContextKeys.ts +++ b/src/vs/workbench/contrib/chat/common/chatContextKeys.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { localize } from '../../../../nls.js'; -import { ContextKeyExpr, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; +import { RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; import { ChatAgentLocation } from './chatAgents.js'; export namespace ChatContextKeys { @@ -41,13 +41,14 @@ export namespace ChatContextKeys { export const languageModelsAreUserSelectable = new RawContextKey('chatModelsAreUserSelectable', false, { type: 'boolean', description: localize('chatModelsAreUserSelectable', "True when the chat model can be selected manually by the user.") }); - export const ChatSetup = { + export const Setup = { signedIn: new RawContextKey('chatSetupSignedIn', false, { type: 'boolean', description: localize('chatSetupSignedIn', "True when chat setup is offered for a signed-in user.") }), entitled: new RawContextKey('chatSetupEntitled', false, { type: 'boolean', description: localize('chatSetupEntitled', "True when chat setup is offered for a signed-in, entitled user.") }), triggered: new RawContextKey('chatSetupTriggered', false, { type: 'boolean', description: localize('chatSetupTriggered', "True when chat setup is triggered.") }), installing: new RawContextKey('chatSetupInstalling', false, { type: 'boolean', description: localize('chatSetupInstalling', "True when chat setup is installing chat.") }), - signingIn: new RawContextKey('chatSetupSigningIn', false, { type: 'boolean', description: localize('chatSetupSigningIn', "True when chat setup is waiting for signing in.") }) + signingIn: new RawContextKey('chatSetupSigningIn', false, { type: 'boolean', description: localize('chatSetupSigningIn', "True when chat setup is waiting for signing in.") }), + + installed: new RawContextKey('chatSetupInstalled', false, { type: 'boolean', description: localize('chatSetupInstalled', "True when the chat extension is installed.") }), }; - export const setupRunning = ContextKeyExpr.or(ChatSetup.triggered, ChatSetup.signingIn, ChatSetup.installing); } diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedService.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedService.ts index a6063cd7a0f93..01a8fe3692b3d 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedService.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedService.ts @@ -231,7 +231,7 @@ export class WalkthroughsService extends Disposable implements IWalkthroughsServ this._register(this.extensionManagementService.onDidInstallExtensions((result) => { - if (result.some(e => ExtensionIdentifier.equals(this.productService.gitHubEntitlement?.extensionId, e.identifier.id) && !e?.context?.[EXTENSION_INSTALL_SKIP_WALKTHROUGH_CONTEXT])) { + if (result.some(e => ExtensionIdentifier.equals(this.productService.defaultChatAgent?.extensionId, e.identifier.id) && !e?.context?.[EXTENSION_INSTALL_SKIP_WALKTHROUGH_CONTEXT])) { result.forEach(e => { this.sessionInstalledExtensions.add(e.identifier.id.toLowerCase()); this.progressByEvent(`extensionInstalled:${e.identifier.id.toLowerCase()}`); From b8a74f167a7ac0c9c4f3c938af7214a17d46321e Mon Sep 17 00:00:00 2001 From: isidorn Date: Thu, 21 Nov 2024 11:14:43 +0100 Subject: [PATCH 025/119] update distro pointer --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b39c6eeee1596..39a7ab93e251f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.96.0", - "distro": "c89663f50091606f884f24839cafd701ddf29260", + "distro": "f656a837bb4aa14329dbe98d58c178615e959ae1", "author": { "name": "Microsoft Corporation" }, From d987732b23b8fa423ac7c413d199f6140d148ee2 Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Thu, 21 Nov 2024 11:58:33 +0100 Subject: [PATCH 026/119] Smoke test failure - changing selectors depending on app quality (#234181) * changing selector depending on app quality smoke test * using correct selector * resetting to just insiders * focusing if not focused after event * removing dom node focus --- src/typings/editContext.d.ts | 4 +- src/vs/editor/common/config/editorOptions.ts | 2 +- .../services/driver/browser/driver.ts | 44 +++++++++++++------ test/automation/src/code.ts | 9 ++-- test/automation/src/debug.ts | 9 ++-- test/automation/src/editor.ts | 15 ++++--- test/automation/src/editors.ts | 3 +- test/automation/src/extensions.ts | 3 +- test/automation/src/notebook.ts | 7 +-- test/automation/src/scm.ts | 16 ++++--- test/automation/src/settings.ts | 14 ++++-- 11 files changed, 85 insertions(+), 41 deletions(-) diff --git a/src/typings/editContext.d.ts b/src/typings/editContext.d.ts index 5b5da0ac7e955..0958584866711 100644 --- a/src/typings/editContext.d.ts +++ b/src/typings/editContext.d.ts @@ -58,8 +58,8 @@ interface EditContextEventHandlersEventMap { type EventHandler = (event: TEvent) => void; -interface TextUpdateEvent extends Event { - new(type: DOMString, options?: TextUpdateEventInit): TextUpdateEvent; +declare class TextUpdateEvent extends Event { + constructor(type: DOMString, options?: TextUpdateEventInit); readonly updateRangeStart: number; readonly updateRangeEnd: number; diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index 0aaa0a5b7e96c..660dafe8df686 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -5751,7 +5751,7 @@ export const EditorOptions = { emptySelectionClipboard: register(new EditorEmptySelectionClipboard()), dropIntoEditor: register(new EditorDropIntoEditor()), experimentalEditContextEnabled: register(new EditorBooleanOption( - EditorOption.experimentalEditContextEnabled, 'experimentalEditContextEnabled', false, + EditorOption.experimentalEditContextEnabled, 'experimentalEditContextEnabled', product.quality !== 'stable', { description: nls.localize('experimentalEditContextEnabled', "Sets whether the new experimental edit context should be used instead of the text area."), included: platform.isChrome || platform.isEdge || platform.isNative diff --git a/src/vs/workbench/services/driver/browser/driver.ts b/src/vs/workbench/services/driver/browser/driver.ts index d78e55aa97373..ad689b9db6f31 100644 --- a/src/vs/workbench/services/driver/browser/driver.ts +++ b/src/vs/workbench/services/driver/browser/driver.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { getClientArea, getTopLeftOffset } from '../../../../base/browser/dom.js'; +import { getClientArea, getTopLeftOffset, isHTMLDivElement, isHTMLTextAreaElement } from '../../../../base/browser/dom.js'; import { mainWindow } from '../../../../base/browser/window.js'; import { coalesce } from '../../../../base/common/arrays.js'; import { language, locale } from '../../../../base/common/platform.js'; @@ -133,18 +133,36 @@ export class BrowserWindowDriver implements IWindowDriver { if (!element) { throw new Error(`Editor not found: ${selector}`); } - - const textarea = element as HTMLTextAreaElement; - const start = textarea.selectionStart; - const newStart = start + text.length; - const value = textarea.value; - const newValue = value.substr(0, start) + text + value.substr(start); - - textarea.value = newValue; - textarea.setSelectionRange(newStart, newStart); - - const event = new Event('input', { 'bubbles': true, 'cancelable': true }); - textarea.dispatchEvent(event); + if (isHTMLDivElement(element)) { + // Edit context is enabled + const editContext = element.editContext; + if (!editContext) { + throw new Error(`Edit context not found: ${selector}`); + } + const selectionStart = editContext.selectionStart; + const selectionEnd = editContext.selectionEnd; + const event = new TextUpdateEvent('textupdate', { + updateRangeStart: selectionStart, + updateRangeEnd: selectionEnd, + text, + selectionStart: selectionStart + text.length, + selectionEnd: selectionStart + text.length, + compositionStart: 0, + compositionEnd: 0 + }); + editContext.dispatchEvent(event); + } else if (isHTMLTextAreaElement(element)) { + const start = element.selectionStart; + const newStart = start + text.length; + const value = element.value; + const newValue = value.substr(0, start) + text + value.substr(start); + + element.value = newValue; + element.setSelectionRange(newStart, newStart); + + const event = new Event('input', { 'bubbles': true, 'cancelable': true }); + element.dispatchEvent(event); + } } async getTerminalBuffer(selector: string): Promise { diff --git a/test/automation/src/code.ts b/test/automation/src/code.ts index a925cdd65bc9b..fd64b7cdeb1a9 100644 --- a/test/automation/src/code.ts +++ b/test/automation/src/code.ts @@ -12,6 +12,7 @@ import { launch as launchPlaywrightBrowser } from './playwrightBrowser'; import { PlaywrightDriver } from './playwrightDriver'; import { launch as launchPlaywrightElectron } from './playwrightElectron'; import { teardown } from './processes'; +import { Quality } from './application'; export interface LaunchOptions { codePath?: string; @@ -28,6 +29,7 @@ export interface LaunchOptions { readonly tracing?: boolean; readonly headless?: boolean; readonly browser?: 'chromium' | 'webkit' | 'firefox'; + readonly quality: Quality; } interface ICodeInstance { @@ -77,7 +79,7 @@ export async function launch(options: LaunchOptions): Promise { const { serverProcess, driver } = await measureAndLog(() => launchPlaywrightBrowser(options), 'launch playwright (browser)', options.logger); registerInstance(serverProcess, options.logger, 'server'); - return new Code(driver, options.logger, serverProcess); + return new Code(driver, options.logger, serverProcess, options.quality); } // Electron smoke tests (playwright) @@ -85,7 +87,7 @@ export async function launch(options: LaunchOptions): Promise { const { electronProcess, driver } = await measureAndLog(() => launchPlaywrightElectron(options), 'launch playwright (electron)', options.logger); registerInstance(electronProcess, options.logger, 'electron'); - return new Code(driver, options.logger, electronProcess); + return new Code(driver, options.logger, electronProcess, options.quality); } } @@ -96,7 +98,8 @@ export class Code { constructor( driver: PlaywrightDriver, readonly logger: Logger, - private readonly mainProcess: cp.ChildProcess + private readonly mainProcess: cp.ChildProcess, + readonly quality: Quality ) { this.driver = new Proxy(driver, { get(target, prop) { diff --git a/test/automation/src/debug.ts b/test/automation/src/debug.ts index b7b7d427f4b0f..e2e227fc35e14 100644 --- a/test/automation/src/debug.ts +++ b/test/automation/src/debug.ts @@ -9,6 +9,7 @@ import { Code, findElement } from './code'; import { Editors } from './editors'; import { Editor } from './editor'; import { IElement } from './driver'; +import { Quality } from './application'; const VIEWLET = 'div[id="workbench.view.debug"]'; const DEBUG_VIEW = `${VIEWLET}`; @@ -31,7 +32,8 @@ const CONSOLE_OUTPUT = `.repl .output.expression .value`; const CONSOLE_EVALUATION_RESULT = `.repl .evaluation-result.expression .value`; const CONSOLE_LINK = `.repl .value a.link`; -const REPL_FOCUSED = '.repl-input-wrapper .monaco-editor textarea'; +const REPL_FOCUSED_NATIVE_EDIT_CONTEXT = '.repl-input-wrapper .monaco-editor .native-edit-context'; +const REPL_FOCUSED_TEXTAREA = '.repl-input-wrapper .monaco-editor textarea'; export interface IStackFrame { name: string; @@ -127,8 +129,9 @@ export class Debug extends Viewlet { async waitForReplCommand(text: string, accept: (result: string) => boolean): Promise { await this.commands.runCommand('Debug: Focus on Debug Console View'); - await this.code.waitForActiveElement(REPL_FOCUSED); - await this.code.waitForSetValue(REPL_FOCUSED, text); + const selector = this.code.quality === Quality.Stable ? REPL_FOCUSED_TEXTAREA : REPL_FOCUSED_NATIVE_EDIT_CONTEXT; + await this.code.waitForActiveElement(selector); + await this.code.waitForSetValue(selector, text); // Wait for the keys to be picked up by the editor model such that repl evaluates what just got typed await this.editor.waitForEditorContents('debug:replinput', s => s.indexOf(text) >= 0); diff --git a/test/automation/src/editor.ts b/test/automation/src/editor.ts index 538866bfc0603..dd6160795650c 100644 --- a/test/automation/src/editor.ts +++ b/test/automation/src/editor.ts @@ -6,6 +6,7 @@ import { References } from './peek'; import { Commands } from './workbench'; import { Code } from './code'; +import { Quality } from './application'; const RENAME_BOX = '.monaco-editor .monaco-editor.rename-box'; const RENAME_INPUT = `${RENAME_BOX} .rename-input`; @@ -78,10 +79,10 @@ export class Editor { async waitForEditorFocus(filename: string, lineNumber: number, selectorPrefix = ''): Promise { const editor = [selectorPrefix || '', EDITOR(filename)].join(' '); const line = `${editor} .view-lines > .view-line:nth-child(${lineNumber})`; - const textarea = `${editor} textarea`; + const editContext = `${editor} ${this._editContextSelector()}`; await this.code.waitAndClick(line, 1, 1); - await this.code.waitForActiveElement(textarea); + await this.code.waitForActiveElement(editContext); } async waitForTypeInEditor(filename: string, text: string, selectorPrefix = ''): Promise { @@ -92,14 +93,18 @@ export class Editor { await this.code.waitForElement(editor); - const textarea = `${editor} textarea`; - await this.code.waitForActiveElement(textarea); + const editContext = `${editor} ${this._editContextSelector()}`; + await this.code.waitForActiveElement(editContext); - await this.code.waitForTypeInEditor(textarea, text); + await this.code.waitForTypeInEditor(editContext, text); await this.waitForEditorContents(filename, c => c.indexOf(text) > -1, selectorPrefix); } + private _editContextSelector() { + return this.code.quality === Quality.Stable ? 'textarea' : '.native-edit-context'; + } + async waitForEditorContents(filename: string, accept: (contents: string) => boolean, selectorPrefix = ''): Promise { const selector = [selectorPrefix || '', `${EDITOR(filename)} .view-lines`].join(' '); return this.code.waitForTextContent(selector, undefined, c => accept(c.replace(/\u00a0/g, ' '))); diff --git a/test/automation/src/editors.ts b/test/automation/src/editors.ts index b3a914ffff026..472385c8534d1 100644 --- a/test/automation/src/editors.ts +++ b/test/automation/src/editors.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { Quality } from './application'; import { Code } from './code'; export class Editors { @@ -53,7 +54,7 @@ export class Editors { } async waitForActiveEditor(fileName: string, retryCount?: number): Promise { - const selector = `.editor-instance .monaco-editor[data-uri$="${fileName}"] textarea`; + const selector = `.editor-instance .monaco-editor[data-uri$="${fileName}"] ${this.code.quality === Quality.Stable ? 'textarea' : '.native-edit-context'}`; return this.code.waitForActiveElement(selector, retryCount); } diff --git a/test/automation/src/extensions.ts b/test/automation/src/extensions.ts index 2a481f9fe766e..c881e4fd8dc6d 100644 --- a/test/automation/src/extensions.ts +++ b/test/automation/src/extensions.ts @@ -8,6 +8,7 @@ import { Code } from './code'; import { ncp } from 'ncp'; import { promisify } from 'util'; import { Commands } from './workbench'; +import { Quality } from './application'; import path = require('path'); import fs = require('fs'); @@ -20,7 +21,7 @@ export class Extensions extends Viewlet { async searchForExtension(id: string): Promise { await this.commands.runCommand('Extensions: Focus on Extensions View', { exactLabelMatch: true }); - await this.code.waitForTypeInEditor('div.extensions-viewlet[id="workbench.view.extensions"] .monaco-editor textarea', `@id:${id}`); + await this.code.waitForTypeInEditor(`div.extensions-viewlet[id="workbench.view.extensions"] .monaco-editor ${this.code.quality === Quality.Stable ? 'textarea' : '.native-edit-context'}`, `@id:${id}`); await this.code.waitForTextContent(`div.part.sidebar div.composite.title h2`, 'Extensions: Marketplace'); let retrials = 1; diff --git a/test/automation/src/notebook.ts b/test/automation/src/notebook.ts index dff250027db72..cd46cbdb0dd49 100644 --- a/test/automation/src/notebook.ts +++ b/test/automation/src/notebook.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { Quality } from './application'; import { Code } from './code'; import { QuickAccess } from './quickaccess'; import { QuickInput } from './quickinput'; @@ -46,10 +47,10 @@ export class Notebook { await this.code.waitForElement(editor); - const textarea = `${editor} textarea`; - await this.code.waitForActiveElement(textarea); + const editContext = `${editor} ${this.code.quality === Quality.Stable ? 'textarea' : '.native-edit-context'}`; + await this.code.waitForActiveElement(editContext); - await this.code.waitForTypeInEditor(textarea, text); + await this.code.waitForTypeInEditor(editContext, text); await this._waitForActiveCellEditorContents(c => c.indexOf(text) > -1); } diff --git a/test/automation/src/scm.ts b/test/automation/src/scm.ts index 9f950f2b16a77..6489badbe8a41 100644 --- a/test/automation/src/scm.ts +++ b/test/automation/src/scm.ts @@ -6,9 +6,11 @@ import { Viewlet } from './viewlet'; import { IElement } from './driver'; import { findElement, findElements, Code } from './code'; +import { Quality } from './application'; const VIEWLET = 'div[id="workbench.view.scm"]'; -const SCM_INPUT = `${VIEWLET} .scm-editor textarea`; +const SCM_INPUT_NATIVE_EDIT_CONTEXT = `${VIEWLET} .scm-editor .native-edit-context`; +const SCM_INPUT_TEXTAREA = `${VIEWLET} .scm-editor textarea`; const SCM_RESOURCE = `${VIEWLET} .monaco-list-row .resource`; const REFRESH_COMMAND = `div[id="workbench.parts.sidebar"] .actions-container a.action-label[aria-label="Refresh"]`; const COMMIT_COMMAND = `div[id="workbench.parts.sidebar"] .actions-container a.action-label[aria-label="Commit"]`; @@ -44,7 +46,7 @@ export class SCM extends Viewlet { async openSCMViewlet(): Promise { await this.code.dispatchKeybinding('ctrl+shift+g'); - await this.code.waitForElement(SCM_INPUT); + await this.code.waitForElement(this._editContextSelector()); } async waitForChange(name: string, type?: string): Promise { @@ -71,9 +73,13 @@ export class SCM extends Viewlet { } async commit(message: string): Promise { - await this.code.waitAndClick(SCM_INPUT); - await this.code.waitForActiveElement(SCM_INPUT); - await this.code.waitForSetValue(SCM_INPUT, message); + await this.code.waitAndClick(this._editContextSelector()); + await this.code.waitForActiveElement(this._editContextSelector()); + await this.code.waitForSetValue(this._editContextSelector(), message); await this.code.waitAndClick(COMMIT_COMMAND); } + + private _editContextSelector(): string { + return this.code.quality === Quality.Stable ? SCM_INPUT_TEXTAREA : SCM_INPUT_NATIVE_EDIT_CONTEXT; + } } diff --git a/test/automation/src/settings.ts b/test/automation/src/settings.ts index 68401eb0edaa3..8cf221b1487b6 100644 --- a/test/automation/src/settings.ts +++ b/test/automation/src/settings.ts @@ -7,8 +7,10 @@ import { Editor } from './editor'; import { Editors } from './editors'; import { Code } from './code'; import { QuickAccess } from './quickaccess'; +import { Quality } from './application'; -const SEARCH_BOX = '.settings-editor .suggest-input-container .monaco-editor textarea'; +const SEARCH_BOX_NATIVE_EDIT_CONTEXT = '.settings-editor .suggest-input-container .monaco-editor .native-edit-context'; +const SEARCH_BOX_TEXTAREA = '.settings-editor .suggest-input-container .monaco-editor textarea'; export class SettingsEditor { constructor(private code: Code, private editors: Editors, private editor: Editor, private quickaccess: QuickAccess) { } @@ -57,13 +59,13 @@ export class SettingsEditor { async openUserSettingsUI(): Promise { await this.quickaccess.runCommand('workbench.action.openSettings2'); - await this.code.waitForActiveElement(SEARCH_BOX); + await this.code.waitForActiveElement(this._editContextSelector()); } async searchSettingsUI(query: string): Promise { await this.openUserSettingsUI(); - await this.code.waitAndClick(SEARCH_BOX); + await this.code.waitAndClick(this._editContextSelector()); if (process.platform === 'darwin') { await this.code.dispatchKeybinding('cmd+a'); } else { @@ -71,7 +73,11 @@ export class SettingsEditor { } await this.code.dispatchKeybinding('Delete'); await this.code.waitForElements('.settings-editor .settings-count-widget', false, results => !results || (results?.length === 1 && !results[0].textContent)); - await this.code.waitForTypeInEditor('.settings-editor .suggest-input-container .monaco-editor textarea', query); + await this.code.waitForTypeInEditor(this._editContextSelector(), query); await this.code.waitForElements('.settings-editor .settings-count-widget', false, results => results?.length === 1 && results[0].textContent.includes('Found')); } + + private _editContextSelector() { + return this.code.quality === Quality.Stable ? SEARCH_BOX_TEXTAREA : SEARCH_BOX_NATIVE_EDIT_CONTEXT; + } } From 9a13d63c624710f71f871c73600beddc8f434ae4 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Thu, 21 Nov 2024 03:05:23 -0800 Subject: [PATCH 027/119] Persist selected chat model globally, not per-workspace (#234335) * Persist selected chat model globally, not per-workspace * Cleanup --- .../contrib/chat/browser/chatEditor.ts | 1 - .../contrib/chat/browser/chatInputPart.ts | 53 ++++++++++++------- .../contrib/chat/browser/chatWidget.ts | 4 +- .../browser/inlineChatController.ts | 36 ++++--------- 4 files changed, 43 insertions(+), 51 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatEditor.ts b/src/vs/workbench/contrib/chat/browser/chatEditor.ts index 69774d4b6db0c..46c63ff9344e1 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditor.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditor.ts @@ -128,7 +128,6 @@ export class ChatEditor extends EditorPane { // Need to set props individually on the memento this._viewState.inputValue = widgetViewState.inputValue; - this._viewState.selectedLanguageModelId = widgetViewState.selectedLanguageModelId; this._memento.saveMemento(); } } diff --git a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts index 76bcd49b9b3b2..b731c63c9d9e3 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts @@ -65,6 +65,7 @@ import { WorkbenchList } from '../../../../platform/list/browser/listService.js' import { ILogService } from '../../../../platform/log/common/log.js'; import { INotificationService } from '../../../../platform/notification/common/notification.js'; import { IOpenerService, type OpenInternalOptions } from '../../../../platform/opener/common/opener.js'; +import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { FolderThemeIcon, IThemeService } from '../../../../platform/theme/common/themeService.js'; import { fillEditorsDragData } from '../../../browser/dnd.js'; import { IFileLabelOptions, ResourceLabels } from '../../../browser/labels.js'; @@ -281,6 +282,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge @IMenuService private readonly menuService: IMenuService, @ILanguageService private readonly languageService: ILanguageService, @IThemeService private readonly themeService: IThemeService, + @IStorageService private readonly storageService: IStorageService, ) { super(); @@ -316,6 +318,35 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this._chatEditsListPool = this._register(this.instantiationService.createInstance(CollapsibleListPool, this._onDidChangeVisibility.event, MenuId.ChatEditingWidgetModifiedFilesToolbar)); this._hasFileAttachmentContextKey = ChatContextKeys.hasFileAttachments.bindTo(contextKeyService); + + this.initSelectedModel(); + } + + private getSelectedModelStorageKey(): string { + return `chat.currentLanguageModel.${this.location}`; + } + + private initSelectedModel() { + const persistedSelection = this.storageService.get(this.getSelectedModelStorageKey(), StorageScope.APPLICATION); + if (persistedSelection) { + const model = this.languageModelsService.lookupLanguageModel(persistedSelection); + if (model) { + this._currentLanguageModel = persistedSelection; + this._onDidChangeCurrentLanguageModel.fire(this._currentLanguageModel); + } else { + this._waitForPersistedLanguageModel.value = this.languageModelsService.onDidChangeLanguageModels(e => { + const persistedModel = e.added?.find(m => m.identifier === persistedSelection); + if (persistedModel) { + this._waitForPersistedLanguageModel.clear(); + + if (persistedModel.metadata.isUserSelectable) { + this._currentLanguageModel = persistedSelection; + this._onDidChangeCurrentLanguageModel.fire(this._currentLanguageModel!); + } + } + }); + } + } } private setCurrentLanguageModelToDefault() { @@ -335,6 +366,8 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge if (this.cachedDimensions) { this.layout(this.cachedDimensions.height, this.cachedDimensions.width); } + + this.storageService.store(this.getSelectedModelStorageKey(), modelId, StorageScope.APPLICATION, StorageTarget.USER); } private loadHistory(): HistoryNavigator2 { @@ -367,26 +400,6 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge if (state.inputValue) { this.setValue(state.inputValue, false); } - - if (state.selectedLanguageModelId) { - const model = this.languageModelsService.lookupLanguageModel(state.selectedLanguageModelId); - if (model) { - this._currentLanguageModel = state.selectedLanguageModelId; - this._onDidChangeCurrentLanguageModel.fire(this._currentLanguageModel); - } else { - this._waitForPersistedLanguageModel.value = this.languageModelsService.onDidChangeLanguageModels(e => { - const persistedModel = e.added?.find(m => m.identifier === state.selectedLanguageModelId); - if (persistedModel) { - this._waitForPersistedLanguageModel.clear(); - - if (persistedModel.metadata.isUserSelectable) { - this._currentLanguageModel = state.selectedLanguageModelId; - this._onDidChangeCurrentLanguageModel.fire(this._currentLanguageModel!); - } - } - }); - } - } } logInputHistory(): void { diff --git a/src/vs/workbench/contrib/chat/browser/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/chatWidget.ts index 75eaaa6669736..2ce9404d4e231 100644 --- a/src/vs/workbench/contrib/chat/browser/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatWidget.ts @@ -60,7 +60,6 @@ const $ = dom.$; export interface IChatViewState { inputValue?: string; inputState?: IChatInputState; - selectedLanguageModelId?: string; } export interface IChatWidgetStyles extends IChatInputStyles { @@ -1261,8 +1260,7 @@ export class ChatWidget extends Disposable implements IChatWidget { getViewState(): IChatViewState { return { inputValue: this.getInput(), - inputState: this.inputPart.getViewState(), - selectedLanguageModelId: this.inputPart.currentLanguageModel, + inputState: this.inputPart.getViewState() }; } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts index 17d1b391b3b0d..796aedb719fd6 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts @@ -33,23 +33,22 @@ import { IContextKey, IContextKeyService } from '../../../../platform/contextkey import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js'; import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; import { ILogService } from '../../../../platform/log/common/log.js'; +import { IEditorService, SIDE_GROUP } from '../../../services/editor/common/editorService.js'; +import { IViewsService } from '../../../services/views/common/viewsService.js'; import { showChatView } from '../../chat/browser/chat.js'; -import { IChatViewState, IChatWidgetLocationOptions } from '../../chat/browser/chatWidget.js'; +import { IChatWidgetLocationOptions } from '../../chat/browser/chatWidget.js'; import { ChatAgentLocation } from '../../chat/common/chatAgents.js'; +import { ChatContextKeys } from '../../chat/common/chatContextKeys.js'; import { ChatModel, ChatRequestRemovalReason, IChatRequestModel, IChatTextEditGroup, IChatTextEditGroupState, IResponse } from '../../chat/common/chatModel.js'; import { IChatService } from '../../chat/common/chatService.js'; -import { HunkInformation, Session, StashedSession } from './inlineChatSession.js'; -import { InlineChatError } from './inlineChatSessionServiceImpl.js'; -import { EditModeStrategy, HunkAction, IEditObserver, LiveStrategy, PreviewStrategy, ProgressingEditsOptions } from './inlineChatStrategies.js'; -import { CTX_INLINE_CHAT_EDITING, CTX_INLINE_CHAT_REQUEST_IN_PROGRESS, CTX_INLINE_CHAT_RESPONSE_TYPE, CTX_INLINE_CHAT_USER_DID_EDIT, CTX_INLINE_CHAT_VISIBLE, EditMode, INLINE_CHAT_ID, InlineChatConfigKeys, InlineChatResponseType } from '../common/inlineChat.js'; import { INotebookEditorService } from '../../notebook/browser/services/notebookEditorService.js'; -import { IEditorService, SIDE_GROUP } from '../../../services/editor/common/editorService.js'; -import { IViewsService } from '../../../services/views/common/viewsService.js'; +import { CTX_INLINE_CHAT_EDITING, CTX_INLINE_CHAT_REQUEST_IN_PROGRESS, CTX_INLINE_CHAT_RESPONSE_TYPE, CTX_INLINE_CHAT_USER_DID_EDIT, CTX_INLINE_CHAT_VISIBLE, EditMode, INLINE_CHAT_ID, InlineChatConfigKeys, InlineChatResponseType } from '../common/inlineChat.js'; import { IInlineChatSavingService } from './inlineChatSavingService.js'; +import { HunkInformation, Session, StashedSession } from './inlineChatSession.js'; import { IInlineChatSessionService } from './inlineChatSessionService.js'; +import { InlineChatError } from './inlineChatSessionServiceImpl.js'; +import { EditModeStrategy, HunkAction, IEditObserver, LiveStrategy, PreviewStrategy, ProgressingEditsOptions } from './inlineChatStrategies.js'; import { InlineChatZoneWidget } from './inlineChatZoneWidget.js'; -import { ChatContextKeys } from '../../chat/common/chatContextKeys.js'; -import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; export const enum State { CREATE_SESSION = 'CREATE_SESSION', @@ -104,8 +103,6 @@ export class InlineChatController implements IEditorContribution { return editor.getContribution(INLINE_CHAT_ID); } - private static readonly _storageKey = 'inlineChatController.state'; - private _isDisposed: boolean = false; private readonly _store = new DisposableStore(); @@ -147,7 +144,6 @@ export class InlineChatController implements IEditorContribution { @IContextKeyService contextKeyService: IContextKeyService, @IChatService private readonly _chatService: IChatService, @IEditorService private readonly _editorService: IEditorService, - @IStorageService private readonly _storageService: IStorageService, @INotebookEditorService notebookEditorService: INotebookEditorService, ) { this._ctxVisible = CTX_INLINE_CHAT_VISIBLE.bindTo(contextKeyService); @@ -411,7 +407,7 @@ export class InlineChatController implements IEditorContribution { this._sessionStore.add(this._session.wholeRange.onDidChange(handleWholeRangeChange)); handleWholeRangeChange(); - this._ui.value.widget.setChatModel(this._session.chatModel, this._retrieveWidgetState()); + this._ui.value.widget.setChatModel(this._session.chatModel); this._updatePlaceholder(); const isModelEmpty = !this._session.chatModel.hasRequests; @@ -893,11 +889,6 @@ export class InlineChatController implements IEditorContribution { this._ctxUserDidEdit.reset(); if (this._ui.rawValue) { - // persist selected LM in memento - const { selectedLanguageModelId } = this._ui.rawValue.widget.chatWidget.getViewState(); - const state = { selectedLanguageModelId }; - this._storageService.store(InlineChatController._storageKey, state, StorageScope.PROFILE, StorageTarget.USER); - this._ui.rawValue.hide(); } @@ -907,15 +898,6 @@ export class InlineChatController implements IEditorContribution { } } - private _retrieveWidgetState(): IChatViewState | undefined { - try { - const state = JSON.parse(this._storageService.get(InlineChatController._storageKey, StorageScope.PROFILE) ?? '{}'); - return state; - } catch { - return undefined; - } - } - private _updateCtxResponseType(): void { if (!this._session) { From 515399c31390dcc8efa5304eb1045bec901dfa82 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 21 Nov 2024 12:18:02 +0100 Subject: [PATCH 028/119] chat - welcome updates (#234329) --- package.json | 2 +- src/vs/base/common/product.ts | 1 + .../contrib/chat/browser/chatSetup.contribution.ts | 11 +++++++---- .../contrib/chat/browser/media/chatViewWelcome.css | 3 --- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 39a7ab93e251f..9c08b11f0c0f0 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.96.0", - "distro": "f656a837bb4aa14329dbe98d58c178615e959ae1", + "distro": "7d14bf7e9a283e1c9ca8b18ccb0c13274a3757cf", "author": { "name": "Microsoft Corporation" }, diff --git a/src/vs/base/common/product.ts b/src/vs/base/common/product.ts index 3cf230772d624..459ffb75a1021 100644 --- a/src/vs/base/common/product.ts +++ b/src/vs/base/common/product.ts @@ -310,6 +310,7 @@ export interface IDefaultChatAgent { readonly chatWelcomeTitle: string; readonly documentationUrl: string; readonly privacyStatementUrl: string; + readonly collectionDocumentationUrl: string; readonly providerId: string; readonly providerName: string; readonly providerScopes: string[]; diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts b/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts index affafca838a8c..8a24d1e2dd104 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts @@ -6,7 +6,7 @@ import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js'; import { Disposable, toDisposable } from '../../../../base/common/lifecycle.js'; import { ContextKeyExpr, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; -import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; +import { ITelemetryService, TelemetryLevel } from '../../../../platform/telemetry/common/telemetry.js'; import { AuthenticationSession, IAuthenticationService } from '../../../services/authentication/common/authentication.js'; import { IProductService } from '../../../../platform/product/common/productService.js'; import { IExtensionManagementService } from '../../../../platform/extensionManagement/common/extensionManagement.js'; @@ -47,6 +47,7 @@ const defaultChat = { chatWelcomeTitle: product.defaultChatAgent?.chatWelcomeTitle ?? '', documentationUrl: product.defaultChatAgent?.documentationUrl ?? '', privacyStatementUrl: product.defaultChatAgent?.privacyStatementUrl ?? '', + collectionDocumentationUrl: product.defaultChatAgent?.collectionDocumentationUrl ?? '', providerId: product.defaultChatAgent?.providerId ?? '', providerName: product.defaultChatAgent?.providerName ?? '', providerScopes: product.defaultChatAgent?.providerScopes ?? [], @@ -102,7 +103,7 @@ class ChatSetupContribution extends Disposable implements IWorkbenchContribution @IProductService private readonly productService: IProductService, @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, @IExtensionService private readonly extensionService: IExtensionService, - @IInstantiationService private readonly instantiationService: IInstantiationService + @IInstantiationService private readonly instantiationService: IInstantiationService, ) { super(); @@ -119,8 +120,10 @@ class ChatSetupContribution extends Disposable implements IWorkbenchContribution } private registerChatWelcome(): void { - const header = localize('setupPreamble1', "{0} is your AI pair programmer that helps you write code faster and smarter.", defaultChat.name); - const footer = localize('setupPreamble2', "By proceeding you agree to the [Privacy Statement]({0}).", defaultChat.privacyStatementUrl); + const header = localize('setupPreamble1', "{0} is your AI pair programmer.", defaultChat.name); + const footer = this.telemetryService.telemetryLevel !== TelemetryLevel.NONE ? + localize({ key: 'setupPreambleWithOptOut', comment: ['{Locked="]({0})"}'] }, "{0} may use your code snippets for product improvements. Read our [privacy statement]({1}) and learn how to [opt out]({2}).", defaultChat.name, defaultChat.privacyStatementUrl, defaultChat.collectionDocumentationUrl) : + localize({ key: 'setupPreambleWithoutOptOut', comment: ['{Locked="]({0})"}'] }, "By proceeding you agree to our [privacy statement]({0}).", defaultChat.privacyStatementUrl); // Setup: Triggered (signed-out) Registry.as(ChatViewsWelcomeExtensions.ChatViewsWelcomeRegistry).register({ diff --git a/src/vs/workbench/contrib/chat/browser/media/chatViewWelcome.css b/src/vs/workbench/contrib/chat/browser/media/chatViewWelcome.css index 4e54182995cc0..e7024c5ba1746 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chatViewWelcome.css +++ b/src/vs/workbench/contrib/chat/browser/media/chatViewWelcome.css @@ -62,7 +62,6 @@ div.chat-welcome-view { & > .chat-welcome-view-progress { display: flex; gap: 6px; - color: var(--vscode-descriptionForeground); text-align: center; max-width: 350px; padding: 0 20px; @@ -70,7 +69,6 @@ div.chat-welcome-view { } & > .chat-welcome-view-message { - color: var(--vscode-descriptionForeground); text-align: center; max-width: 350px; padding: 0 20px; @@ -88,7 +86,6 @@ div.chat-welcome-view { } & > .chat-welcome-view-tips { - color: var(--vscode-descriptionForeground); max-width: 250px; margin-top: 10px; From ec41c7350dd1d8fdbdde16662062e8874bbe87a0 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Thu, 21 Nov 2024 12:21:34 +0100 Subject: [PATCH 029/119] Git - add git blame hover (status bar item, editor decoration) (#234338) --- extensions/git/src/blame.ts | 50 +++++++++++++++++++++++++++++++--- extensions/git/src/commands.ts | 11 +++++++- 2 files changed, 56 insertions(+), 5 deletions(-) diff --git a/extensions/git/src/blame.ts b/extensions/git/src/blame.ts index d116922d9bf4a..10c8e0d98f21e 100644 --- a/extensions/git/src/blame.ts +++ b/extensions/git/src/blame.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { DecorationOptions, l10n, Position, Range, TextEditor, TextEditorChange, TextEditorDecorationType, TextEditorChangeKind, ThemeColor, Uri, window, workspace, EventEmitter, ConfigurationChangeEvent, StatusBarItem, StatusBarAlignment, Command } from 'vscode'; +import { DecorationOptions, l10n, Position, Range, TextEditor, TextEditorChange, TextEditorDecorationType, TextEditorChangeKind, ThemeColor, Uri, window, workspace, EventEmitter, ConfigurationChangeEvent, StatusBarItem, StatusBarAlignment, Command, MarkdownString } from 'vscode'; import { Model } from './model'; import { dispose, fromNow, IDisposable, pathEquals } from './util'; import { Repository } from './repository'; @@ -86,6 +86,41 @@ function processTextEditorChangesWithBlameInformation(blameInformation: BlameInf return changesWithBlameInformation; } +function getBlameInformationHover(documentUri: Uri, blameInformation: BlameInformation | string): MarkdownString { + if (typeof blameInformation === 'string') { + return new MarkdownString(blameInformation, true); + } + + const markdownString = new MarkdownString(); + markdownString.supportThemeIcons = true; + markdownString.isTrusted = true; + + if (blameInformation.authorName) { + markdownString.appendMarkdown(`$(account) **${blameInformation.authorName}**`); + + if (blameInformation.date) { + const dateString = new Date(blameInformation.date).toLocaleString(undefined, { year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric' }); + markdownString.appendMarkdown(`, $(history) ${fromNow(blameInformation.date, true, true)} (${dateString})`); + } + + markdownString.appendMarkdown('\n\n'); + } + + markdownString.appendMarkdown(`${blameInformation.message}\n\n`); + markdownString.appendMarkdown(`---\n\n`); + + markdownString.appendMarkdown(`[$(eye) View Commit](command:git.blameStatusBarItem.viewCommit?${encodeURIComponent(JSON.stringify([documentUri, blameInformation.id]))})`); + markdownString.appendMarkdown('  |  '); + markdownString.appendMarkdown(`[$(copy) ${blameInformation.id.substring(0, 8)}](command:git.blameStatusBarItem.copyContent?${encodeURIComponent(JSON.stringify(blameInformation.id))})`); + + if (blameInformation.message) { + markdownString.appendMarkdown('  '); + markdownString.appendMarkdown(`[$(copy) Message](command:git.blameStatusBarItem.copyContent?${encodeURIComponent(JSON.stringify(blameInformation.message))})`); + } + + return markdownString; +} + interface RepositoryBlameInformation { readonly commit: string; /* commit used for blame information */ readonly blameInformation: Map; @@ -314,16 +349,19 @@ class GitBlameEditorDecoration { const contentText = typeof blame.blameInformation === 'string' ? blame.blameInformation : `${blame.blameInformation.message ?? ''}, ${blame.blameInformation.authorName ?? ''} (${fromNow(blame.blameInformation.date ?? Date.now(), true, true)})`; - return this._createDecoration(blame.lineNumber, contentText); + const hoverMessage = getBlameInformationHover(textEditor.document.uri, blame.blameInformation); + + return this._createDecoration(blame.lineNumber, contentText, hoverMessage); }); textEditor.setDecorations(this._decorationType, decorations); } - private _createDecoration(lineNumber: number, contentText: string): DecorationOptions { + private _createDecoration(lineNumber: number, contentText: string, hoverMessage: MarkdownString): DecorationOptions { const position = new Position(lineNumber, Number.MAX_SAFE_INTEGER); return { + hoverMessage, range: new Range(position, position), renderOptions: { after: { @@ -389,6 +427,7 @@ class GitBlameStatusBarItem { if (!this._statusBarItem) { this._statusBarItem = window.createStatusBarItem('git.blame', StatusBarAlignment.Right, 200); + this._statusBarItem.name = l10n.t('Git Blame Information'); this._disposables.push(this._statusBarItem); } @@ -400,11 +439,14 @@ class GitBlameStatusBarItem { if (typeof blameInformation[0].blameInformation === 'string') { this._statusBarItem.text = `$(git-commit) ${blameInformation[0].blameInformation}`; + this._statusBarItem.tooltip = getBlameInformationHover(textEditor.document.uri, blameInformation[0].blameInformation); + this._statusBarItem.command = undefined; } else { this._statusBarItem.text = `$(git-commit) ${blameInformation[0].blameInformation.authorName ?? ''} (${fromNow(blameInformation[0].blameInformation.date ?? new Date(), true, true)})`; + this._statusBarItem.tooltip = getBlameInformationHover(textEditor.document.uri, blameInformation[0].blameInformation); this._statusBarItem.command = { title: l10n.t('View Commit'), - command: 'git.statusBar.viewCommit', + command: 'git.blameStatusBarItem.viewCommit', arguments: [textEditor.document.uri, blameInformation[0].blameInformation.id] } satisfies Command; } diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index 84d9b30f7b722..43518a7784393 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -4307,7 +4307,7 @@ export class CommandCenter { env.clipboard.writeText(historyItem.message); } - @command('git.statusBar.viewCommit', { repository: true }) + @command('git.blameStatusBarItem.viewCommit', { repository: true }) async viewStatusBarCommit(repository: Repository, historyItemId: string): Promise { if (!repository || !historyItemId) { return; @@ -4325,6 +4325,15 @@ export class CommandCenter { await commands.executeCommand('_workbench.openMultiDiffEditor', { multiDiffSourceUri, title, resources }); } + @command('git.blameStatusBarItem.copyContent') + async blameStatusBarCopyContent(content: string): Promise { + if (typeof content !== 'string') { + return; + } + + env.clipboard.writeText(content); + } + private createCommand(id: string, key: string, method: Function, options: ScmCommandOptions): (...args: any[]) => any { const result = (...args: any[]) => { let result: Promise; From 01adf93c6b4fd4ea68658a1c94c17a0476169411 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Thu, 21 Nov 2024 12:27:50 +0100 Subject: [PATCH 030/119] Restore tab newline fix in editor tabs (#234340) bring back tab newline fix --- .../browser/parts/editor/media/editortabscontrol.css | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/vs/workbench/browser/parts/editor/media/editortabscontrol.css b/src/vs/workbench/browser/parts/editor/media/editortabscontrol.css index 39d489d4c41dc..aa684a520bb43 100644 --- a/src/vs/workbench/browser/parts/editor/media/editortabscontrol.css +++ b/src/vs/workbench/browser/parts/editor/media/editortabscontrol.css @@ -15,6 +15,11 @@ flex: 1; } +.monaco-workbench .part.editor > .content .editor-group-container > .title .title-label .label-name, +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab .tab-label .label-name { + white-space: nowrap; +} + .monaco-workbench .part.editor > .content .editor-group-container > .title .title-label a, .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab .tab-label a { font-size: 13px; From b4b34c1456c3818b2752c9c242588dd86ef29355 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 21 Nov 2024 13:15:29 +0100 Subject: [PATCH 031/119] chat - tweaks to welcome (#234343) --- .../chat/browser/chatSetup.contribution.ts | 10 ++++++---- .../chat/browser/media/chatViewWelcome.css | 15 ++++++--------- .../viewsWelcome/chatViewWelcomeController.ts | 15 +++------------ .../chat/browser/viewsWelcome/chatViewsWelcome.ts | 2 +- 4 files changed, 16 insertions(+), 26 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts b/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts index 8a24d1e2dd104..fb6f22da1105a 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts @@ -171,12 +171,13 @@ class ChatSetupContribution extends Disposable implements IWorkbenchContribution ChatContextKeys.Setup.installed.negate() )!, icon: defaultChat.icon, - progress: localize('setupChatSigningIn', "Signing in to {0}...", defaultChat.providerName), + disableFirstLinkToButton: true, content: new MarkdownString([ header, + localize('setupChatSigningIn', "$(loading~spin) Signing in to {0}...", defaultChat.providerName), footer, `[${localize('learnMore', "Learn More")}](${defaultChat.documentationUrl})`, - ].join('\n\n'), { isTrusted: true }), + ].join('\n\n'), { isTrusted: true, supportThemeIcons: true }), }); // Setup: Installing @@ -184,12 +185,13 @@ class ChatSetupContribution extends Disposable implements IWorkbenchContribution title: defaultChat.chatWelcomeTitle, when: ChatContextKeys.Setup.installing, icon: defaultChat.icon, - progress: localize('setupChatInstalling', "Setting up Chat for you..."), + disableFirstLinkToButton: true, content: new MarkdownString([ header, + localize('setupChatInstalling', "$(loading~spin) Setting up Chat for you..."), footer, `[${localize('learnMore', "Learn More")}](${defaultChat.documentationUrl})`, - ].join('\n\n'), { isTrusted: true }), + ].join('\n\n'), { isTrusted: true, supportThemeIcons: true }), }); } diff --git a/src/vs/workbench/contrib/chat/browser/media/chatViewWelcome.css b/src/vs/workbench/contrib/chat/browser/media/chatViewWelcome.css index e7024c5ba1746..dffc746a0072c 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chatViewWelcome.css +++ b/src/vs/workbench/contrib/chat/browser/media/chatViewWelcome.css @@ -59,15 +59,6 @@ div.chat-welcome-view { font-size: 11px; } - & > .chat-welcome-view-progress { - display: flex; - gap: 6px; - text-align: center; - max-width: 350px; - padding: 0 20px; - margin-top: 20px; - } - & > .chat-welcome-view-message { text-align: center; max-width: 350px; @@ -77,6 +68,12 @@ div.chat-welcome-view { a { color: var(--vscode-textLink-foreground); } + + .codicon[class*='codicon-'] { + font-size: 13px; + line-height: 1.4em; + vertical-align: bottom; + } } .monaco-button { diff --git a/src/vs/workbench/contrib/chat/browser/viewsWelcome/chatViewWelcomeController.ts b/src/vs/workbench/contrib/chat/browser/viewsWelcome/chatViewWelcomeController.ts index 09cbdccb926f2..c68a5442d8cca 100644 --- a/src/vs/workbench/contrib/chat/browser/viewsWelcome/chatViewWelcomeController.ts +++ b/src/vs/workbench/contrib/chat/browser/viewsWelcome/chatViewWelcomeController.ts @@ -17,7 +17,6 @@ import { IInstantiationService } from '../../../../../platform/instantiation/com import { ILogService } from '../../../../../platform/log/common/log.js'; import { IOpenerService } from '../../../../../platform/opener/common/opener.js'; import { defaultButtonStyles } from '../../../../../platform/theme/browser/defaultStyles.js'; -import { spinningLoading } from '../../../../../platform/theme/common/iconRegistry.js'; import { ChatAgentLocation } from '../../common/chatAgents.js'; import { chatViewsWelcomeRegistry, IChatViewsWelcomeDescriptor } from './chatViewsWelcome.js'; @@ -89,7 +88,7 @@ export class ChatViewWelcomeController extends Disposable { icon: enabledDescriptor.icon, title: enabledDescriptor.title, message: enabledDescriptor.content, - progress: enabledDescriptor.progress + disableFirstLinkToButton: enabledDescriptor.disableFirstLinkToButton, }; const welcomeView = this.renderDisposables.add(this.instantiationService.createInstance(ChatViewWelcomePart, content, { firstLinkToButton: true, location: this.location })); this.element!.appendChild(welcomeView.element); @@ -104,7 +103,7 @@ export interface IChatViewWelcomeContent { icon?: ThemeIcon; title: string; message: IMarkdownString; - progress?: string; + disableFirstLinkToButton?: boolean; tips?: IMarkdownString; } @@ -144,7 +143,7 @@ export class ChatViewWelcomePart extends Disposable { title.textContent = content.title; const renderer = this.instantiationService.createInstance(MarkdownRenderer, {}); const messageResult = this._register(renderer.render(content.message)); - const firstLink = (options?.firstLinkToButton && !content.progress) ? messageResult.element.querySelector('a') : undefined; + const firstLink = options?.firstLinkToButton && !content.disableFirstLinkToButton ? messageResult.element.querySelector('a') : undefined; if (firstLink) { const target = firstLink.getAttribute('data-href'); const button = this._register(new Button(firstLink.parentElement!, defaultButtonStyles)); @@ -159,14 +158,6 @@ export class ChatViewWelcomePart extends Disposable { dom.append(message, messageResult.element); - if (content.progress) { - const progress = dom.append(this.element, $('.chat-welcome-view-progress')); - progress.appendChild(renderIcon(spinningLoading)); - - const progressLabel = dom.append(progress, $('span')); - progressLabel.textContent = content.progress; - } - if (content.tips) { const tips = dom.append(this.element, $('.chat-welcome-view-tips')); const tipsResult = this._register(renderer.render(content.tips)); diff --git a/src/vs/workbench/contrib/chat/browser/viewsWelcome/chatViewsWelcome.ts b/src/vs/workbench/contrib/chat/browser/viewsWelcome/chatViewsWelcome.ts index d522a1029d2bc..b08baae1460bc 100644 --- a/src/vs/workbench/contrib/chat/browser/viewsWelcome/chatViewsWelcome.ts +++ b/src/vs/workbench/contrib/chat/browser/viewsWelcome/chatViewsWelcome.ts @@ -17,7 +17,7 @@ export interface IChatViewsWelcomeDescriptor { icon?: ThemeIcon; title: string; content: IMarkdownString; - progress?: string; + disableFirstLinkToButton?: boolean; when: ContextKeyExpression; } From 043762fe5b4eadfbaeaf2134112a695411787d57 Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Thu, 21 Nov 2024 13:23:29 +0100 Subject: [PATCH 032/119] edits undo: chat entries are no longer hidden after reload (#234345) edits: chat entries are no longer hidden after reload --- .../chat/browser/actions/chatClearActions.ts | 8 --- .../chatEditingModifiedFileEntry.ts | 25 ++++---- .../browser/chatEditing/chatEditingSession.ts | 63 ++++++++++--------- .../contrib/chat/common/chatEditingService.ts | 1 - 4 files changed, 45 insertions(+), 52 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatClearActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatClearActions.ts index b84f3df93e0f7..746d4d74e3dc0 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatClearActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatClearActions.ts @@ -265,15 +265,11 @@ export function registerNewChatActions() { async run(accessor: ServicesAccessor, ...args: any[]) { const chatEditingService = accessor.get(IChatEditingService); - const chatWidgetService = accessor.get(IChatWidgetService); const currentEditingSession = chatEditingService.currentEditingSession; if (!currentEditingSession) { return; } - - const widget = chatWidgetService.getWidgetBySessionId(currentEditingSession.chatSessionId); await currentEditingSession.undoInteraction(); - widget?.viewModel?.model.disableRequests(currentEditingSession.hiddenRequestIds.get()); } }); @@ -297,15 +293,11 @@ export function registerNewChatActions() { async run(accessor: ServicesAccessor, ...args: any[]) { const chatEditingService = accessor.get(IChatEditingService); - const chatWidgetService = accessor.get(IChatWidgetService); const currentEditingSession = chatEditingService.currentEditingSession; if (!currentEditingSession) { return; } - - const widget = chatWidgetService.getWidgetBySessionId(currentEditingSession.chatSessionId); await chatEditingService.currentEditingSession?.redoInteraction(); - widget?.viewModel?.model.disableRequests(currentEditingSession.hiddenRequestIds.get()); } }); diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts index 83766889f96d4..defe157b7b7fc 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts @@ -40,7 +40,7 @@ export class ChatEditingModifiedFileEntry extends Disposable implements IModifie public readonly entryId = `${ChatEditingModifiedFileEntry.scheme}::${++ChatEditingModifiedFileEntry.lastEntryId}`; private readonly docSnapshot: ITextModel; - private readonly originalContent; + public readonly initialContent: string; private readonly doc: ITextModel; private readonly docFileEditorModel: IResolvedTextFileEditorModel; private _allEditsAreFromUs: boolean = true; @@ -121,7 +121,7 @@ export class ChatEditingModifiedFileEntry extends Disposable implements IModifie private readonly _multiDiffEntryDelegate: { collapse: (transaction: ITransaction | undefined) => void }, private _telemetryInfo: IModifiedEntryTelemetryInfo, kind: ChatEditKind, - originalContent: string | undefined, + initialContent: string | undefined, @IModelService modelService: IModelService, @ITextModelService textModelService: ITextModelService, @ILanguageService languageService: ILanguageService, @@ -137,10 +137,10 @@ export class ChatEditingModifiedFileEntry extends Disposable implements IModifie this.docFileEditorModel = this._register(resourceRef).object as IResolvedTextFileEditorModel; this.doc = resourceRef.object.textEditorModel; - this.originalContent = originalContent ?? this.doc.getValue(); + this.initialContent = initialContent ?? this.doc.getValue(); const docSnapshot = this.docSnapshot = this._register( modelService.createModel( - createTextBufferFactoryFromSnapshot(originalContent ? stringToSnapshot(originalContent) : this.doc.createSnapshot()), + createTextBufferFactoryFromSnapshot(initialContent ? stringToSnapshot(initialContent) : this.doc.createSnapshot()), languageService.createById(this.doc.getLanguageId()), ChatEditingTextModelContentProvider.getFileURI(this.entryId, this.modifiedURI.path), false @@ -192,7 +192,6 @@ export class ChatEditingModifiedFileEntry extends Disposable implements IModifie telemetryInfo: this._telemetryInfo }; } - restoreFromSnapshot(snapshot: ISnapshotEntry) { this._stateObs.set(snapshot.state, undefined); this.docSnapshot.setValue(snapshot.original); @@ -200,8 +199,8 @@ export class ChatEditingModifiedFileEntry extends Disposable implements IModifie this._edit = snapshot.originalToCurrentEdit; } - resetToInitialValue(value: string) { - this._setDocValue(value); + resetToInitialValue() { + this._setDocValue(this.initialContent); } acceptStreamingEditsStart(tx: ITransaction) { @@ -262,7 +261,7 @@ export class ChatEditingModifiedFileEntry extends Disposable implements IModifie } if (!this.isCurrentlyBeingModified.get()) { - const didResetToOriginalContent = this.doc.getValue() === this.originalContent; + const didResetToOriginalContent = this.doc.getValue() === this.initialContent; const currentState = this._stateObs.get(); switch (currentState) { case WorkingSetEntryState.Modified: @@ -427,11 +426,11 @@ export class ChatEditingModifiedFileEntry extends Disposable implements IModifie } export interface IModifiedEntryTelemetryInfo { - agentId: string | undefined; - command: string | undefined; - sessionId: string; - requestId: string; - result: IChatAgentResult | undefined; + readonly agentId: string | undefined; + readonly command: string | undefined; + readonly sessionId: string; + readonly requestId: string; + readonly result: IChatAgentResult | undefined; } export interface ISnapshotEntry { diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts index d2df122bba607..261f2b54db498 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts @@ -43,6 +43,7 @@ import { IEnvironmentService } from '../../../../../platform/environment/common/ import { VSBuffer } from '../../../../../base/common/buffer.js'; import { IOffsetEdit, ISingleOffsetEdit, OffsetEdit } from '../../../../../editor/common/core/offsetEdit.js'; import { ILogService } from '../../../../../platform/log/common/log.js'; +import { IChatService } from '../../common/chatService.js'; const STORAGE_CONTENTS_FOLDER = 'contents'; const STORAGE_STATE_FILE = 'state.json'; @@ -56,8 +57,6 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio * Contains the contents of a file when the AI first began doing edits to it. */ private readonly _initialFileContents = new ResourceMap(); - private readonly _snapshots = new Map(); - private readonly _filesToSkipCreating = new ResourceSet(); private readonly _entriesObs = observableValue(this, []); @@ -144,6 +143,7 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio @IChatAgentService private readonly _chatAgentService: IChatAgentService, @IEnvironmentService private readonly _environmentService: IEnvironmentService, @ILogService private readonly _logService: ILogService, + @IChatService private readonly _chatService: IChatService, ) { super(); @@ -211,10 +211,13 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio } } + private _findSnapshot(requestId: string): IChatEditingSessionSnapshot | undefined { + return this._linearHistory.get().find(s => s.requestId === requestId); + } + public createSnapshot(requestId: string | undefined): void { const snapshot = this._createSnapshot(requestId); if (requestId) { - this._snapshots.set(requestId, snapshot); for (const workingSetItem of this._workingSet.keys()) { this._workingSet.set(workingSetItem, { state: WorkingSetEntryState.Sent }); } @@ -248,7 +251,7 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio } public async getSnapshotModel(requestId: string, snapshotUri: URI): Promise { - const entries = this._snapshots.get(requestId)?.entries; + const entries = this._findSnapshot(requestId)?.entries; if (!entries) { return null; } @@ -262,7 +265,7 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio } public getSnapshot(requestId: string, uri: URI) { - const snapshot = this._snapshots.get(requestId); + const snapshot = this._findSnapshot(requestId); const snapshotEntries = snapshot?.entries; return snapshotEntries?.get(uri); } @@ -273,7 +276,7 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio private _pendingSnapshot: IChatEditingSessionSnapshot | undefined; public async restoreSnapshot(requestId: string | undefined): Promise { if (requestId !== undefined) { - const snapshot = this._snapshots.get(requestId); + const snapshot = this._findSnapshot(requestId); if (snapshot) { if (!this._pendingSnapshot) { // Create and save a pending snapshot @@ -301,10 +304,7 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio for (const entry of this._entriesObs.get()) { const snapshotEntry = snapshot.entries.get(entry.modifiedURI); if (!snapshotEntry) { - const initialContents = this._initialFileContents.get(entry.modifiedURI); - if (typeof initialContents === 'string') { - entry.resetToInitialValue(initialContents); - } + entry.resetToInitialValue(); entry.dispose(); } } @@ -313,8 +313,7 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio // Restore all entries from the snapshot for (const snapshotEntry of snapshot.entries.values()) { const entry = await this._getOrCreateModifiedFileEntry(snapshotEntry.resource, snapshotEntry.telemetryInfo); - entry.restoreFromSnapshot(snapshotEntry); - entriesArr.push(entry); + entry.restoreFromSnapshot(snapshotEntry); entriesArr.push(entry); } this._entriesObs.set(entriesArr, undefined); @@ -412,12 +411,14 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio async _performStop(): Promise { // Close out all open files - await Promise.allSettled(this._editorGroupsService.groups.map(async (g) => { - return Promise.allSettled(g.editors.map(async (e) => { - if (e instanceof MultiDiffEditorInput || e instanceof DiffEditorInput && (e.original.resource?.scheme === ChatEditingModifiedFileEntry.scheme || e.original.resource?.scheme === ChatEditingTextModelContentProvider.scheme)) { + const schemes = [ChatEditingModifiedFileEntry.scheme, ChatEditingTextModelContentProvider.scheme]; + await Promise.allSettled(this._editorGroupsService.groups.flatMap(async (g) => { + return g.editors.map(async (e) => { + if ((e instanceof MultiDiffEditorInput && e.initialResources?.some(r => r.originalUri && schemes.indexOf(r.originalUri.scheme) !== -1)) + || (e instanceof DiffEditorInput && e.original.resource && schemes.indexOf(e.original.resource.scheme) !== -1)) { await g.closeEditor(e); } - })); + }); })); // delete the persisted editing session state @@ -501,6 +502,8 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio const previousSnapshot = linearHistory[linearHistoryIndex - 1]; await this.restoreSnapshot(previousSnapshot.requestId); this._linearHistoryIndex.set(linearHistoryIndex - 1, undefined); + this._updateRequestHiddenState(); + } async redoInteraction(): Promise { @@ -515,6 +518,12 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio } await this.restoreSnapshot(nextSnapshot.requestId); this._linearHistoryIndex.set(linearHistoryIndex + 1, undefined); + this._updateRequestHiddenState(); + } + + private _updateRequestHiddenState() { + const hiddenRequestIds = this._linearHistory.get().slice(this._linearHistoryIndex.get()).map(s => s.requestId).filter((r): r is string => !!r); + this._chatService.getSession(this.chatSessionId)?.disableRequests(hiddenRequestIds); } private async _acceptStreamingEditsStart(): Promise { @@ -578,12 +587,11 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio } return existingEntry; } - - const originalContent = this._initialFileContents.get(resource); + const initialContent = this._initialFileContents.get(resource); // This gets manually disposed in .dispose() or in .restoreSnapshot() - const entry = await this._createModifiedFileEntry(resource, responseModel, false, originalContent); - if (!originalContent) { - this._initialFileContents.set(resource, entry.modifiedModel.getValue()); + const entry = await this._createModifiedFileEntry(resource, responseModel, false, initialContent); + if (!initialContent) { + this._initialFileContents.set(resource, entry.initialContent); } // If an entry is deleted e.g. reverting a created file, // remove it from the entries and don't show it in the working set anymore @@ -602,11 +610,11 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio return entry; } - private async _createModifiedFileEntry(resource: URI, responseModel: IModifiedEntryTelemetryInfo, mustExist = false, originalContent: string | undefined): Promise { + private async _createModifiedFileEntry(resource: URI, responseModel: IModifiedEntryTelemetryInfo, mustExist = false, initialContent: string | undefined): Promise { try { const ref = await this._textModelService.createModelReference(resource); - return this._instantiationService.createInstance(ChatEditingModifiedFileEntry, ref, { collapse: (transaction: ITransaction | undefined) => this._collapse(resource, transaction) }, responseModel, mustExist ? ChatEditKind.Created : ChatEditKind.Modified, originalContent); + return this._instantiationService.createInstance(ChatEditingModifiedFileEntry, ref, { collapse: (transaction: ITransaction | undefined) => this._collapse(resource, transaction) }, responseModel, mustExist ? ChatEditKind.Created : ChatEditKind.Modified, initialContent); } catch (err) { if (mustExist) { throw err; @@ -614,7 +622,7 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio // this file does not exist yet, create it and try again await this._bulkEditService.apply({ edits: [{ newResource: resource }] }); this._editorService.openEditor({ resource, options: { inactive: true, preserveFocus: true, pinned: true } }); - return this._createModifiedFileEntry(resource, responseModel, true, originalContent); + return this._createModifiedFileEntry(resource, responseModel, true, initialContent); } } @@ -680,15 +688,9 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio this._filesToSkipCreating.clear(); this._initialFileContents.clear(); - this._snapshots.clear(); this._pendingSnapshot = undefined; const snapshotsFromHistory = await Promise.all(data.linearHistory.map(deserializeChatEditingSessionSnapshot)); - for (const snapshot of snapshotsFromHistory) { - if (snapshot.requestId) { - this._snapshots.set(snapshot.requestId, snapshot); - } - } data.filesToSkipCreating.forEach((uriStr: string) => { this._filesToSkipCreating.add(URI.parse(uriStr)); }); @@ -700,6 +702,7 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio const pendingSnapshot = await deserializeChatEditingSessionSnapshot(data.pendingSnapshot); this._restoreSnapshot(pendingSnapshot); this._state.set(ChatEditingSessionState.Idle, undefined); + this._updateRequestHiddenState(); return true; } catch (e) { this._logService.error(`Error restoring chat editing session from ${storageLocation.toString()}`, e); diff --git a/src/vs/workbench/contrib/chat/common/chatEditingService.ts b/src/vs/workbench/contrib/chat/common/chatEditingService.ts index 731d6d0bfb172..5b3b80e085283 100644 --- a/src/vs/workbench/contrib/chat/common/chatEditingService.ts +++ b/src/vs/workbench/contrib/chat/common/chatEditingService.ts @@ -74,7 +74,6 @@ export interface IChatEditingSession { readonly onDidDispose: Event; readonly state: IObservable; readonly entries: IObservable; - readonly hiddenRequestIds: IObservable; readonly workingSet: ResourceMap; readonly isVisible: boolean; addFileToWorkingSet(uri: URI, description?: string, kind?: WorkingSetEntryState.Transient | WorkingSetEntryState.Suggested): void; From 8166c5674b795af552d8a57a80d97b3f0fe3464f Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Thu, 21 Nov 2024 16:10:04 +0100 Subject: [PATCH 033/119] chat: persist 'isHidden' (#234350) --- src/vs/workbench/contrib/chat/common/chatModel.ts | 4 ++++ .../common/__snapshots__/ChatService_can_deserialize.0.snap | 1 + .../ChatService_can_deserialize_with_response.0.snap | 1 + .../common/__snapshots__/ChatService_can_serialize.1.snap | 2 ++ .../common/__snapshots__/ChatService_sendRequest_fails.0.snap | 1 + 5 files changed, 9 insertions(+) diff --git a/src/vs/workbench/contrib/chat/common/chatModel.ts b/src/vs/workbench/contrib/chat/common/chatModel.ts index 41f3a8f1595b5..c70bdf6ab0454 100644 --- a/src/vs/workbench/contrib/chat/common/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatModel.ts @@ -660,6 +660,7 @@ export interface ISerializableChatRequestData { /** Is really like "prompt data". This is the message in the format in which the agent gets it + variable values. */ variableData: IChatRequestVariableData; response: ReadonlyArray | undefined; + isHidden: boolean; responseId?: string; agent?: ISerializableChatAgentData; slashCommand?: IChatAgentCommand; @@ -1004,6 +1005,7 @@ export class ChatModel extends Disposable implements IChatModel { // Old messages don't have variableData, or have it in the wrong (non-array) shape const variableData: IChatRequestVariableData = this.reviveVariableData(raw.variableData); const request = new ChatRequestModel(this, parsedRequest, variableData, raw.timestamp ?? -1, undefined, undefined, undefined, undefined, undefined, undefined, raw.requestId); + request.isHidden = !!raw.isHidden; if (raw.response || raw.result || (raw as any).responseErrorDetails) { const agent = (raw.agent && 'metadata' in raw.agent) ? // Check for the new format, ignore entries in the old format reviveSerializedAgent(raw.agent) : undefined; @@ -1013,6 +1015,7 @@ export class ChatModel extends Disposable implements IChatModel { // eslint-disable-next-line local/code-no-dangerous-type-assertions { errorDetails: raw.responseErrorDetails } as IChatAgentResult : raw.result; request.response = new ChatResponseModel(raw.response ?? [new MarkdownString(raw.response)], this, agent, raw.slashCommand, request.id, true, raw.isCanceled, raw.vote, raw.voteDownReason, result, raw.followups, undefined, undefined, raw.responseId); + request.response.isHidden = !!raw.isHidden; if (raw.usedContext) { // @ulugbekna: if this's a new vscode sessions, doc versions are incorrect anyway? request.response.applyReference(revive(raw.usedContext)); } @@ -1282,6 +1285,7 @@ export class ChatModel extends Disposable implements IChatModel { }) : undefined, responseId: r.response?.id, + isHidden: r.isHidden, result: r.response?.result, followups: r.response?.followups, isCanceled: r.response?.isCanceled, diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_deserialize.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_deserialize.0.snap index 7bf0d5055861d..d6eda2dc105d2 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_deserialize.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_deserialize.0.snap @@ -57,6 +57,7 @@ variableData: { variables: [ ] }, response: [ ], responseId: undefined, + isHidden: false, result: { metadata: { metadataKey: "value" } }, followups: undefined, isCanceled: false, diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_deserialize_with_response.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_deserialize_with_response.0.snap index 3f9c0004d5027..f42a981fc872c 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_deserialize_with_response.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_deserialize_with_response.0.snap @@ -57,6 +57,7 @@ variableData: { variables: [ ] }, response: [ ], responseId: undefined, + isHidden: false, result: { errorDetails: { message: "No activated agent with id \"ChatProviderWithUsedContext\"" } }, followups: undefined, isCanceled: false, diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_serialize.1.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_serialize.1.snap index 6a2f07aa1b411..8ad16f37294b7 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_serialize.1.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_serialize.1.snap @@ -57,6 +57,7 @@ variableData: { variables: [ ] }, response: [ ], responseId: undefined, + isHidden: false, result: { metadata: { metadataKey: "value" } }, followups: [ { @@ -130,6 +131,7 @@ variableData: { variables: [ ] }, response: [ ], responseId: undefined, + isHidden: false, result: { }, followups: [ ], isCanceled: false, diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_sendRequest_fails.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_sendRequest_fails.0.snap index 2f1c69f5174b7..a4eeca5350e2d 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_sendRequest_fails.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_sendRequest_fails.0.snap @@ -57,6 +57,7 @@ variableData: { variables: [ ] }, response: [ ], responseId: undefined, + isHidden: false, result: { errorDetails: { message: "No activated agent with id \"ChatProviderWithUsedContext\"" } }, followups: undefined, isCanceled: false, From d6e1e9a383983626a5b659ea5b27fda2d1fca432 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 21 Nov 2024 16:50:23 +0100 Subject: [PATCH 034/119] chat - move the hide action to the dropdown (#234359) --- .../chat/browser/chatSetup.contribution.ts | 36 ++++++++++++++----- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts b/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts index fb6f22da1105a..7cf68461ea498 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts @@ -39,6 +39,8 @@ import { IViewDescriptorService, ViewContainerLocation } from '../../../common/v import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; import { getActiveElement } from '../../../../base/browser/dom.js'; import { ILogService } from '../../../../platform/log/common/log.js'; +import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; +import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js'; const defaultChat = { extensionId: product.defaultChatAgent?.extensionId ?? '', @@ -140,7 +142,7 @@ class ChatSetupContribution extends Disposable implements IWorkbenchContribution header, `[${localize('signInAndSetup', "Sign in to use {0}", defaultChat.name)}](command:${ChatSetupSignInAndInstallChatAction.ID})`, footer, - `[${localize('learnMore', "Learn More")}](${defaultChat.documentationUrl}) | [${localize('hideSetup', "Hide")}](command:${ChatSetupHideAction.ID} "${localize('hideSetup', "Hide")}")`, + `[${localize('learnMore', "Learn More")}](${defaultChat.documentationUrl})`, ].join('\n\n'), { isTrusted: true }), }); @@ -159,7 +161,7 @@ class ChatSetupContribution extends Disposable implements IWorkbenchContribution header, `[${localize('setup', "Install {0}", defaultChat.name)}](command:${ChatSetupInstallAction.ID})`, footer, - `[${localize('learnMore', "Learn More")}](${defaultChat.documentationUrl}) | [${localize('hideSetup', "Hide")}](command:${ChatSetupHideAction.ID} "${localize('hideSetup', "Hide")}")`, + `[${localize('learnMore', "Learn More")}](${defaultChat.documentationUrl})`, ].join('\n\n'), { isTrusted: true }) }); @@ -401,8 +403,7 @@ class ChatSetupTriggerAction extends Action2 { constructor() { super({ id: ChatSetupTriggerAction.ID, - title: ChatSetupTriggerAction.TITLE, - f1: false + title: ChatSetupTriggerAction.TITLE }); } @@ -419,13 +420,18 @@ class ChatSetupTriggerAction extends Action2 { class ChatSetupHideAction extends Action2 { static readonly ID = 'workbench.action.chat.hideSetup'; - static readonly TITLE = localize2('hideChatSetup', "Hide Chat Setup"); + static readonly TITLE = localize2('hideChatSetup', "Hide {0}", defaultChat.name); constructor() { super({ id: ChatSetupHideAction.ID, title: ChatSetupHideAction.TITLE, - f1: false + menu: { + id: MenuId.ChatCommandCenter, + group: 'a_first', + order: 1, + when: ChatContextKeys.Setup.installed.negate() + } }); } @@ -433,6 +439,18 @@ class ChatSetupHideAction extends Action2 { const viewsDescriptorService = accessor.get(IViewDescriptorService); const layoutService = accessor.get(IWorkbenchLayoutService); const instantiationService = accessor.get(IInstantiationService); + const configurationService = accessor.get(IConfigurationService); + const dialogService = accessor.get(IDialogService); + + const { confirmed } = await dialogService.confirm({ + message: localize('hideChatSetupConfirm', "Are you sure you want to hide {0}?", defaultChat.name), + detail: localize('hideChatSetupDetail', "You can restore chat controls from the 'chat.commandCenter.enabled' setting."), + primaryButton: localize('hideChatSetup', "Hide {0}", defaultChat.name) + }); + + if (!confirmed) { + return; + } const location = viewsDescriptorService.getViewLocationById(ChatViewId); @@ -444,6 +462,8 @@ class ChatSetupHideAction extends Action2 { layoutService.setPartHidden(true, Parts.AUXILIARYBAR_PART); // hide if there are no views in the secondary sidebar } } + + configurationService.updateValue('chat.commandCenter.enabled', false); } } @@ -459,7 +479,7 @@ class ChatSetupInstallAction extends Action2 { category: CHAT_CATEGORY, menu: { id: MenuId.ChatCommandCenter, - group: 'a_open', + group: 'a_first', order: 0, when: ContextKeyExpr.and( ChatContextKeys.Setup.signedIn, @@ -525,7 +545,7 @@ class ChatSetupSignInAndInstallChatAction extends Action2 { category: CHAT_CATEGORY, menu: { id: MenuId.ChatCommandCenter, - group: 'a_open', + group: 'a_first', order: 0, when: ContextKeyExpr.and( ChatContextKeys.Setup.signedIn.negate(), From d37cdc8a726deb23cf85383189c3ac3521064260 Mon Sep 17 00:00:00 2001 From: Joyce Er Date: Thu, 21 Nov 2024 08:20:06 -0800 Subject: [PATCH 035/119] fix: chat related files polish (#234364) * fix: don't re-add suggested files that were ignored by user * fix: don't include suggested files in the working set list count * fix: don't change an attached file back to being suggested * fix: italicize suggested file entries --- .../browser/chatContentParts/chatReferencesContentPart.ts | 2 +- .../chat/browser/chatEditing/chatEditingSession.ts | 2 +- src/vs/workbench/contrib/chat/browser/chatInputPart.ts | 8 +++++++- src/vs/workbench/contrib/chat/browser/chatWidget.ts | 2 +- 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatReferencesContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatReferencesContentPart.ts index 42de7cc59a0bd..e05b69b29af3d 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatReferencesContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatReferencesContentPart.ts @@ -394,7 +394,7 @@ class CollapsibleListRenderer implements IListRenderer= remainingFileEntriesBudget) { // The user tried to attach too many files, we have to drop anything after the limit const entriesToPreserve: IChatCollapsibleListItem[] = []; @@ -1136,9 +1138,13 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge // so that the Add Files button remains enabled and the user can easily // override the suggestions with their own manual file selections entries = [...entriesToPreserve, ...newEntriesThatFit, ...suggestedFilesThatFit]; + suggestedFilesInWorkingSetCount = suggestedFilesThatFit.length; + } else { + suggestedFilesInWorkingSetCount = entries.filter(e => e.kind === 'reference' && e.state === WorkingSetEntryState.Suggested).length; } if (entries.length > 1) { - overviewFileCount.textContent = ' ' + localize('chatEditingSession.manyFiles', '({0} files)', entries.length); + const fileCount = entries.length - suggestedFilesInWorkingSetCount; + overviewFileCount.textContent = ' ' + (fileCount === 1 ? localize('chatEditingSession.oneFile', '(1 file)') : localize('chatEditingSession.manyFiles', '({0} files)', fileCount)); } if (excludedEntries.length > 0) { diff --git a/src/vs/workbench/contrib/chat/browser/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/chatWidget.ts index 2ce9404d4e231..58d2543794da0 100644 --- a/src/vs/workbench/contrib/chat/browser/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatWidget.ts @@ -1051,7 +1051,7 @@ export class ChatWidget extends Disposable implements IChatWidget { actualSize: number; }; this.telemetryService.publicLog2('chatEditing/workingSetSize', { originalSize: this.inputPart.attemptedWorkingSetEntriesCount, actualSize: uniqueWorkingSetEntries.size }); - currentEditingSession?.remove(WorkingSetEntryRemovalReason.Programmatic, ...unconfirmedSuggestions); + currentEditingSession?.remove(WorkingSetEntryRemovalReason.User, ...unconfirmedSuggestions); } const result = await this.chatService.sendRequest(this.viewModel.sessionId, input, { From 4e8160f935bd7b2ee0857c67f295dfb3e8a08833 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Thu, 21 Nov 2024 08:26:16 -0800 Subject: [PATCH 036/119] Add telemetry for tool calls (#234365) * Add telemetry for tool calls * Fix test --- .../chat/browser/languageModelToolsService.ts | 106 ++++++++++++------ .../chat/common/languageModelToolsService.ts | 2 + .../tools/languageModelToolsContribution.ts | 1 + .../browser/languageModelToolsService.test.ts | 18 +-- 4 files changed, 85 insertions(+), 42 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/languageModelToolsService.ts b/src/vs/workbench/contrib/chat/browser/languageModelToolsService.ts index 9f3860a6f754d..2b3f6b2dc497d 100644 --- a/src/vs/workbench/contrib/chat/browser/languageModelToolsService.ts +++ b/src/vs/workbench/contrib/chat/browser/languageModelToolsService.ts @@ -6,13 +6,14 @@ import { renderStringAsPlaintext } from '../../../../base/browser/markdownRenderer.js'; import { RunOnceScheduler } from '../../../../base/common/async.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; -import { CancellationError } from '../../../../base/common/errors.js'; +import { CancellationError, isCancellationError } from '../../../../base/common/errors.js'; import { Emitter } from '../../../../base/common/event.js'; import { Iterable } from '../../../../base/common/iterator.js'; import { Disposable, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; import { localize } from '../../../../nls.js'; import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js'; +import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { IExtensionService } from '../../../services/extensions/common/extensions.js'; import { ChatModel } from '../common/chatModel.js'; import { ChatToolInvocation } from '../common/chatProgressTypes/chatToolInvocation.js'; @@ -41,6 +42,7 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo @IContextKeyService private readonly _contextKeyService: IContextKeyService, @IChatService private readonly _chatService: IChatService, @IDialogService private readonly _dialogService: IDialogService, + @ITelemetryService private readonly _telemetryService: ITelemetryService, ) { super(); @@ -138,44 +140,82 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo // Shortcut to write to the model directly here, but could call all the way back to use the real stream. let toolInvocation: ChatToolInvocation | undefined; - if (dto.context) { - const model = this._chatService.getSession(dto.context?.sessionId) as ChatModel; - const request = model.getRequests().at(-1)!; - - const prepared = tool.impl.prepareToolInvocation ? - await tool.impl.prepareToolInvocation(dto.parameters, token) - : undefined; - - const defaultMessage = localize('toolInvocationMessage', "Using {0}", `"${tool.data.displayName}"`); - const invocationMessage = prepared?.invocationMessage ?? defaultMessage; - toolInvocation = new ChatToolInvocation(invocationMessage, prepared?.confirmationMessages); - token.onCancellationRequested(() => { - toolInvocation!.confirmed.complete(false); - }); - model.acceptResponseProgress(request, toolInvocation); - if (prepared?.confirmationMessages) { - const userConfirmed = await toolInvocation.confirmed.p; - if (!userConfirmed) { - throw new CancellationError(); + + try { + if (dto.context) { + const model = this._chatService.getSession(dto.context?.sessionId) as ChatModel; + const request = model.getRequests().at(-1)!; + + const prepared = tool.impl.prepareToolInvocation ? + await tool.impl.prepareToolInvocation(dto.parameters, token) + : undefined; + + const defaultMessage = localize('toolInvocationMessage', "Using {0}", `"${tool.data.displayName}"`); + const invocationMessage = prepared?.invocationMessage ?? defaultMessage; + toolInvocation = new ChatToolInvocation(invocationMessage, prepared?.confirmationMessages); + token.onCancellationRequested(() => { + toolInvocation!.confirmed.complete(false); + }); + model.acceptResponseProgress(request, toolInvocation); + if (prepared?.confirmationMessages) { + const userConfirmed = await toolInvocation.confirmed.p; + if (!userConfirmed) { + throw new CancellationError(); + } } - } - } else { - const prepared = tool.impl.prepareToolInvocation ? - await tool.impl.prepareToolInvocation(dto.parameters, token) - : undefined; - - if (prepared?.confirmationMessages) { - const result = await this._dialogService.confirm({ message: prepared.confirmationMessages.title, detail: renderStringAsPlaintext(prepared.confirmationMessages.message) }); - if (!result.confirmed) { - throw new CancellationError(); + } else { + const prepared = tool.impl.prepareToolInvocation ? + await tool.impl.prepareToolInvocation(dto.parameters, token) + : undefined; + + if (prepared?.confirmationMessages) { + const result = await this._dialogService.confirm({ message: prepared.confirmationMessages.title, detail: renderStringAsPlaintext(prepared.confirmationMessages.message) }); + if (!result.confirmed) { + throw new CancellationError(); + } } } - } - try { - return await tool.impl.invoke(dto, countTokens, token); + + const result = await tool.impl.invoke(dto, countTokens, token); + this._telemetryService.publicLog2( + 'languageModelToolInvoked', + { + result: 'success', + chatSessionId: dto.context?.sessionId, + toolId: tool.data.id, + toolExtensionId: tool.data.extensionId?.value, + }); + return result; + } catch (err) { + const result = isCancellationError(err) ? 'userCancelled' : 'error'; + this._telemetryService.publicLog2( + 'languageModelToolInvoked', + { + result, + chatSessionId: dto.context?.sessionId, + toolId: tool.data.id, + toolExtensionId: tool.data.extensionId?.value, + }); + throw err; } finally { toolInvocation?.isCompleteDeferred.complete(); } } } + +type LanguageModelToolInvokedEvent = { + result: 'success' | 'error' | 'userCancelled'; + chatSessionId: string | undefined; + toolId: string; + toolExtensionId: string | undefined; +}; + +type LanguageModelToolInvokedClassification = { + result: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether invoking the LanguageModelTool resulted in an error.' }; + chatSessionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The ID of the chat session that the tool was used within, if applicable.' }; + toolId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The ID of the tool used.' }; + toolExtensionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The extension that contributed the tool.' }; + owner: 'roblourens'; + comment: 'Provides insight into the usage of language model tools.'; +}; diff --git a/src/vs/workbench/contrib/chat/common/languageModelToolsService.ts b/src/vs/workbench/contrib/chat/common/languageModelToolsService.ts index 6699d50ee5963..f2251ca8166f7 100644 --- a/src/vs/workbench/contrib/chat/common/languageModelToolsService.ts +++ b/src/vs/workbench/contrib/chat/common/languageModelToolsService.ts @@ -11,10 +11,12 @@ import { IDisposable } from '../../../../base/common/lifecycle.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; import { URI } from '../../../../base/common/uri.js'; import { ContextKeyExpression } from '../../../../platform/contextkey/common/contextkey.js'; +import { ExtensionIdentifier } from '../../../../platform/extensions/common/extensions.js'; import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; export interface IToolData { id: string; + extensionId?: ExtensionIdentifier; toolReferenceName?: string; icon?: { dark: URI; light?: URI } | ThemeIcon; when?: ContextKeyExpression; diff --git a/src/vs/workbench/contrib/chat/common/tools/languageModelToolsContribution.ts b/src/vs/workbench/contrib/chat/common/tools/languageModelToolsContribution.ts index d48a1b54011bd..de36f417e6a4f 100644 --- a/src/vs/workbench/contrib/chat/common/tools/languageModelToolsContribution.ts +++ b/src/vs/workbench/contrib/chat/common/tools/languageModelToolsContribution.ts @@ -173,6 +173,7 @@ export class LanguageModelToolsExtensionPointHandler implements IWorkbenchContri const tool: IToolData = { ...rawTool, + extensionId: extension.description.identifier, inputSchema: rawTool.inputSchema, id: rawTool.name, icon, diff --git a/src/vs/workbench/contrib/chat/test/browser/languageModelToolsService.test.ts b/src/vs/workbench/contrib/chat/test/browser/languageModelToolsService.test.ts index 48e21693f0238..9679c7a890893 100644 --- a/src/vs/workbench/contrib/chat/test/browser/languageModelToolsService.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/languageModelToolsService.test.ts @@ -6,14 +6,12 @@ import * as assert from 'assert'; import { CancellationToken } from '../../../../../base/common/cancellation.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; -import { TestConfigurationService } from '../../../../../platform/configuration/test/common/testConfigurationService.js'; -import { ContextKeyService } from '../../../../../platform/contextkey/browser/contextKeyService.js'; import { ContextKeyEqualsExpr, IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; -import { TestExtensionService } from '../../../../test/common/workbenchTestServices.js'; -import { IToolData, IToolImpl, IToolInvocation } from '../../common/languageModelToolsService.js'; -import { MockChatService } from '../common/mockChatService.js'; -import { TestDialogService } from '../../../../../platform/dialogs/test/common/testDialogService.js'; +import { workbenchInstantiationService } from '../../../../test/browser/workbenchTestServices.js'; import { LanguageModelToolsService } from '../../browser/languageModelToolsService.js'; +import { IToolData, IToolImpl, IToolInvocation } from '../../common/languageModelToolsService.js'; +import { ContextKeyService } from '../../../../../platform/contextkey/browser/contextKeyService.js'; +import { TestConfigurationService } from '../../../../../platform/configuration/test/common/testConfigurationService.js'; suite('LanguageModelToolsService', () => { const store = ensureNoDisposablesAreLeakedInTestSuite(); @@ -22,9 +20,11 @@ suite('LanguageModelToolsService', () => { let service: LanguageModelToolsService; setup(() => { - const extensionService = new TestExtensionService(); - contextKeyService = store.add(new ContextKeyService(new TestConfigurationService())); - service = store.add(new LanguageModelToolsService(extensionService, contextKeyService, new MockChatService(), new TestDialogService())); + const instaService = workbenchInstantiationService({ + contextKeyService: () => store.add(new ContextKeyService(new TestConfigurationService)) + }, store); + contextKeyService = instaService.get(IContextKeyService); + service = store.add(instaService.createInstance(LanguageModelToolsService)); }); test('registerToolData', () => { From 5ab1a6a64d02fa99b1c20d2b03e6b38abf82658b Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Thu, 21 Nov 2024 17:38:13 +0100 Subject: [PATCH 037/119] chat edits: fix for redo after reload (#234366) --- .../browser/chatEditing/chatEditingService.ts | 16 ++--- .../browser/chatEditing/chatEditingSession.ts | 71 ++++++++++--------- .../contrib/chat/browser/chatWidget.ts | 2 +- .../contrib/chat/common/chatEditingService.ts | 2 +- 4 files changed, 42 insertions(+), 49 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingService.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingService.ts index 74966fc973b3f..a90172eacd107 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingService.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingService.ts @@ -31,7 +31,6 @@ import { IEditorGroup, IEditorGroupsService } from '../../../../services/editor/ import { IEditorService } from '../../../../services/editor/common/editorService.js'; import { ILifecycleService } from '../../../../services/lifecycle/common/lifecycle.js'; import { MultiDiffEditor } from '../../../multiDiffEditor/browser/multiDiffEditor.js'; -import { MultiDiffEditorInput } from '../../../multiDiffEditor/browser/multiDiffEditorInput.js'; import { IMultiDiffSourceResolver, IMultiDiffSourceResolverService, IResolvedMultiDiffSource, MultiDiffEditorItem } from '../../../multiDiffEditor/browser/multiDiffSourceResolverService.js'; import { ChatAgentLocation, IChatAgentService } from '../../common/chatAgents.js'; import { ChatContextKeys } from '../../common/chatContextKeys.js'; @@ -186,7 +185,7 @@ export class ChatEditingService extends Disposable implements IChatEditingServic super.dispose(); } - async startOrContinueEditingSession(chatSessionId: string, options?: { silent: boolean }): Promise { + async startOrContinueEditingSession(chatSessionId: string): Promise { const session = this._currentSessionObs.get(); if (session) { if (session.chatSessionId === chatSessionId) { @@ -195,10 +194,10 @@ export class ChatEditingService extends Disposable implements IChatEditingServic await session.stop(); } } - return this._createEditingSession(chatSessionId, options); + return this._createEditingSession(chatSessionId); } - private async _createEditingSession(chatSessionId: string, options?: { silent: boolean }): Promise { + private async _createEditingSession(chatSessionId: string): Promise { if (this._currentSessionObs.get()) { throw new BugIndicatingError('Cannot have more than one active editing session'); } @@ -208,14 +207,7 @@ export class ChatEditingService extends Disposable implements IChatEditingServic // listen for completed responses, run the code mapper and apply the edits to this edit session this._currentSessionDisposables.add(this.installAutoApplyObserver(chatSessionId)); - const input = MultiDiffEditorInput.fromResourceMultiDiffEditorInput({ - multiDiffSource: ChatEditingMultiDiffSourceResolver.getMultiDiffSourceUri(), - label: localize('multiDiffEditorInput.name', "Suggested Edits") - }, this._instantiationService); - - const editorPane = options?.silent ? undefined : await this._editorGroupsService.activeGroup.openEditor(input, { pinned: true, activation: EditorActivation.ACTIVATE }) as MultiDiffEditor | undefined; - - const session = this._instantiationService.createInstance(ChatEditingSession, chatSessionId, editorPane, this._editingSessionFileLimitPromise); + const session = this._instantiationService.createInstance(ChatEditingSession, chatSessionId, this._editingSessionFileLimitPromise); await session.restoreState(); this._currentSessionDisposables.add(session.onDidDispose(() => { diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts index df0ee33226c1e..220fc5a30bfd1 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts @@ -32,7 +32,6 @@ import { MultiDiffEditorInput } from '../../../multiDiffEditor/browser/multiDiff import { ChatAgentLocation, IChatAgentService } from '../../common/chatAgents.js'; import { ChatEditingSessionChangeType, ChatEditingSessionState, ChatEditKind, IChatEditingSession, WorkingSetDisplayMetadata, WorkingSetEntryRemovalReason, WorkingSetEntryState } from '../../common/chatEditingService.js'; import { IChatResponseModel } from '../../common/chatModel.js'; -import { IChatWidgetService } from '../chat.js'; import { ChatEditingMultiDiffSourceResolver } from './chatEditingService.js'; import { ChatEditingModifiedFileEntry, IModifiedEntryTelemetryInfo, ISnapshotEntry } from './chatEditingModifiedFileEntry.js'; import { ChatEditingTextModelContentProvider } from './chatEditingTextModelContentProviders.js'; @@ -81,6 +80,8 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio private _removedTransientEntries = new ResourceSet(); + private _editorPane: MultiDiffEditor | undefined; + get state(): IObservable { return this._state; } @@ -122,12 +123,11 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio get isVisible(): boolean { this._assertNotDisposed(); - return Boolean(this.editorPane && this.editorPane.isVisible()); + return Boolean(this._editorPane && this._editorPane.isVisible()); } constructor( public readonly chatSessionId: string, - private editorPane: MultiDiffEditor | undefined, private editingSessionFileLimitPromise: Promise, @IInstantiationService private readonly _instantiationService: IInstantiationService, @IModelService private readonly _modelService: IModelService, @@ -136,7 +136,6 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio @IBulkEditService public readonly _bulkEditService: IBulkEditService, @IEditorGroupsService private readonly _editorGroupsService: IEditorGroupsService, @IEditorService private readonly _editorService: IEditorService, - @IChatWidgetService chatWidgetService: IChatWidgetService, @IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService, @IFileService private readonly _fileService: IFileService, @IFileDialogService private readonly _dialogService: IFileDialogService, @@ -147,11 +146,6 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio ) { super(); - const widget = chatWidgetService.getWidgetBySessionId(chatSessionId); - if (!widget) { - return; // Shouldn't happen - } - // Add the currently active editors to the working set this._trackCurrentEditorsInWorkingSet(); this._register(this._editorService.onDidVisibleEditorsChange(() => { @@ -313,7 +307,8 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio // Restore all entries from the snapshot for (const snapshotEntry of snapshot.entries.values()) { const entry = await this._getOrCreateModifiedFileEntry(snapshotEntry.resource, snapshotEntry.telemetryInfo); - entry.restoreFromSnapshot(snapshotEntry); entriesArr.push(entry); + entry.restoreFromSnapshot(snapshotEntry); + entriesArr.push(entry); } this._entriesObs.set(entriesArr, undefined); @@ -383,21 +378,20 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio async show(): Promise { this._assertNotDisposed(); - - if (this.editorPane?.isVisible()) { - return; - } else if (this.editorPane?.input) { - await this._editorGroupsService.activeGroup.openEditor(this.editorPane.input, { pinned: true, activation: EditorActivation.ACTIVATE }); - return; + if (this._editorPane) { + if (this._editorPane.isVisible()) { + return; + } else if (this._editorPane.input) { + await this._editorGroupsService.activeGroup.openEditor(this._editorPane.input, { pinned: true, activation: EditorActivation.ACTIVATE }); + return; + } } - const input = MultiDiffEditorInput.fromResourceMultiDiffEditorInput({ multiDiffSource: ChatEditingMultiDiffSourceResolver.getMultiDiffSourceUri(), label: localize('multiDiffEditorInput.name', "Suggested Edits") }, this._instantiationService); - const editorPane = await this._editorGroupsService.activeGroup.openEditor(input, { pinned: true, activation: EditorActivation.ACTIVATE }) as MultiDiffEditor | undefined; - this.editorPane = editorPane; + this._editorPane = await this._editorGroupsService.activeGroup.openEditor(input, { pinned: true, activation: EditorActivation.ACTIVATE }) as MultiDiffEditor | undefined; } private stopPromise: Promise | undefined; @@ -495,29 +489,29 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio async undoInteraction(): Promise { const linearHistory = this._linearHistory.get(); - const linearHistoryIndex = this._linearHistoryIndex.get(); - if (linearHistoryIndex <= 0) { + const newIndex = this._linearHistoryIndex.get() - 1; + if (newIndex < 0) { return; } - const previousSnapshot = linearHistory[linearHistoryIndex - 1]; + const previousSnapshot = linearHistory[newIndex]; await this.restoreSnapshot(previousSnapshot.requestId); - this._linearHistoryIndex.set(linearHistoryIndex - 1, undefined); + this._linearHistoryIndex.set(newIndex, undefined); this._updateRequestHiddenState(); } async redoInteraction(): Promise { const linearHistory = this._linearHistory.get(); - const linearHistoryIndex = this._linearHistoryIndex.get(); - if (linearHistoryIndex >= linearHistory.length) { + const newIndex = this._linearHistoryIndex.get() + 1; + if (newIndex > linearHistory.length) { return; } - const nextSnapshot = (linearHistoryIndex + 1 < linearHistory.length ? linearHistory[linearHistoryIndex + 1] : this._pendingSnapshot); + const nextSnapshot = newIndex < linearHistory.length ? linearHistory[newIndex] : this._pendingSnapshot; if (!nextSnapshot) { return; } await this.restoreSnapshot(nextSnapshot.requestId); - this._linearHistoryIndex.set(linearHistoryIndex + 1, undefined); + this._linearHistoryIndex.set(newIndex, undefined); this._updateRequestHiddenState(); } @@ -627,9 +621,9 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio } private _collapse(resource: URI, transaction: ITransaction | undefined) { - const multiDiffItem = this.editorPane?.findDocumentDiffItem(resource); + const multiDiffItem = this._editorPane?.findDocumentDiffItem(resource); if (multiDiffItem) { - this.editorPane?.viewModel?.items.get().find((documentDiffItem) => + this._editorPane?.viewModel?.items.get().find((documentDiffItem) => isEqual(documentDiffItem.originalUri, multiDiffItem.originalUri) && isEqual(documentDiffItem.modifiedUri, multiDiffItem.modifiedUri)) ?.collapsed.set(true, transaction); @@ -685,10 +679,12 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio this._logService.debug(`chatEditingSession: Restoring editing session at ${stateFilePath.toString()}`); const stateFileContent = await this._fileService.readFile(stateFilePath); const data = JSON.parse(stateFileContent.value.toString()) as IChatEditingSessionDTO; + if (data.version !== STORAGE_VERSION) { + return false; + } this._filesToSkipCreating.clear(); this._initialFileContents.clear(); - this._pendingSnapshot = undefined; const snapshotsFromHistory = await Promise.all(data.linearHistory.map(deserializeChatEditingSessionSnapshot)); data.filesToSkipCreating.forEach((uriStr: string) => { @@ -697,10 +693,10 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio for (const fileContentDTO of data.initialFileContents) { this._initialFileContents.set(URI.parse(fileContentDTO[0]), await getFileContent(fileContentDTO[1])); } + this._pendingSnapshot = data.pendingSnapshot ? await deserializeChatEditingSessionSnapshot(data.pendingSnapshot) : undefined; + this._restoreSnapshot(await deserializeChatEditingSessionSnapshot(data.recentSnapshot)); this._linearHistoryIndex.set(data.linearHistoryIndex, undefined); this._linearHistory.set(snapshotsFromHistory, undefined); - const pendingSnapshot = await deserializeChatEditingSessionSnapshot(data.pendingSnapshot); - this._restoreSnapshot(pendingSnapshot); this._state.set(ChatEditingSessionState.Idle, undefined); this._updateRequestHiddenState(); return true; @@ -767,13 +763,14 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio }; try { - const pendingSnapshot = this._createSnapshot(undefined); const data = { + version: STORAGE_VERSION, sessionId: this.chatSessionId, linearHistory: this._linearHistory.get().map(serializeChatEditingSessionSnapshot), linearHistoryIndex: this._linearHistoryIndex.get(), initialFileContents: serializeResourceMap(this._initialFileContents, value => addFileContent(value)), - pendingSnapshot: serializeChatEditingSessionSnapshot(pendingSnapshot), + pendingSnapshot: this._pendingSnapshot ? serializeChatEditingSessionSnapshot(this._pendingSnapshot) : undefined, + recentSnapshot: serializeChatEditingSessionSnapshot(this._createSnapshot(undefined)), filesToSkipCreating: Array.from(this._filesToSkipCreating.keys()).map(uri => uri.toString()), } satisfies IChatEditingSessionDTO; @@ -833,11 +830,15 @@ interface IModifiedEntryTelemetryInfoDTO { type ResourceMapDTO = [string, T][]; +const STORAGE_VERSION = 1; + interface IChatEditingSessionDTO { + readonly version: number; readonly sessionId: string; - readonly pendingSnapshot: IChatEditingSessionSnapshotDTO; + readonly recentSnapshot: IChatEditingSessionSnapshotDTO; readonly linearHistory: IChatEditingSessionSnapshotDTO[]; readonly linearHistoryIndex: number; + readonly pendingSnapshot: IChatEditingSessionSnapshotDTO | undefined; readonly initialFileContents: ResourceMapDTO; readonly filesToSkipCreating: string[]; } diff --git a/src/vs/workbench/contrib/chat/browser/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/chatWidget.ts index 58d2543794da0..3b0aaa58dc859 100644 --- a/src/vs/workbench/contrib/chat/browser/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatWidget.ts @@ -283,7 +283,7 @@ export class ChatWidget extends Disposable implements IChatWidget { const sessionId = this._viewModel?.sessionId; if (sessionId) { if (sessionId !== currentEditSession?.chatSessionId) { - currentEditSession = await this.chatEditingService.startOrContinueEditingSession(sessionId, { silent: true }); + currentEditSession = await this.chatEditingService.startOrContinueEditingSession(sessionId); } } else { if (currentEditSession) { diff --git a/src/vs/workbench/contrib/chat/common/chatEditingService.ts b/src/vs/workbench/contrib/chat/common/chatEditingService.ts index 5b3b80e085283..74f7d6cd270d3 100644 --- a/src/vs/workbench/contrib/chat/common/chatEditingService.ts +++ b/src/vs/workbench/contrib/chat/common/chatEditingService.ts @@ -36,7 +36,7 @@ export interface IChatEditingService { readonly editingSessionFileLimit: number; - startOrContinueEditingSession(chatSessionId: string, options?: { silent: boolean }): Promise; + startOrContinueEditingSession(chatSessionId: string): Promise; getEditingSession(resource: URI): IChatEditingSession | null; createSnapshot(requestId: string): void; getSnapshotUri(requestId: string, uri: URI): URI | undefined; From b441c40439734856725ecb06d6acee8bb547474e Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 21 Nov 2024 18:09:20 +0100 Subject: [PATCH 038/119] chat - tweaks to welcome (#234370) --- src/vs/base/common/product.ts | 1 + .../contrib/chat/browser/chatSetup.contribution.ts | 13 ++++++++----- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/vs/base/common/product.ts b/src/vs/base/common/product.ts index 459ffb75a1021..fcb54c94eb396 100644 --- a/src/vs/base/common/product.ts +++ b/src/vs/base/common/product.ts @@ -318,4 +318,5 @@ export interface IDefaultChatAgent { readonly entitlementChatEnabled: string; readonly entitlementSkuKey: string; readonly entitlementSku30DTrialValue: string; + readonly entitlementSkuAlternateUrl: string; } diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts b/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts index 7cf68461ea498..df27d811fdd82 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts @@ -6,7 +6,7 @@ import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js'; import { Disposable, toDisposable } from '../../../../base/common/lifecycle.js'; import { ContextKeyExpr, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; -import { ITelemetryService, TelemetryLevel } from '../../../../platform/telemetry/common/telemetry.js'; +import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { AuthenticationSession, IAuthenticationService } from '../../../services/authentication/common/authentication.js'; import { IProductService } from '../../../../platform/product/common/productService.js'; import { IExtensionManagementService } from '../../../../platform/extensionManagement/common/extensionManagement.js'; @@ -57,6 +57,7 @@ const defaultChat = { entitlementSkuKey: product.defaultChatAgent?.entitlementSkuKey ?? '', entitlementSku30DTrialValue: product.defaultChatAgent?.entitlementSku30DTrialValue ?? '', entitlementChatEnabled: product.defaultChatAgent?.entitlementChatEnabled ?? '', + entitlementSkuAlternateUrl: product.defaultChatAgent?.entitlementSkuAlternateUrl ?? '' }; type ChatSetupEntitlementEnablementClassification = { @@ -122,10 +123,8 @@ class ChatSetupContribution extends Disposable implements IWorkbenchContribution } private registerChatWelcome(): void { - const header = localize('setupPreamble1', "{0} is your AI pair programmer.", defaultChat.name); - const footer = this.telemetryService.telemetryLevel !== TelemetryLevel.NONE ? - localize({ key: 'setupPreambleWithOptOut', comment: ['{Locked="]({0})"}'] }, "{0} may use your code snippets for product improvements. Read our [privacy statement]({1}) and learn how to [opt out]({2}).", defaultChat.name, defaultChat.privacyStatementUrl, defaultChat.collectionDocumentationUrl) : - localize({ key: 'setupPreambleWithoutOptOut', comment: ['{Locked="]({0})"}'] }, "By proceeding you agree to our [privacy statement]({0}).", defaultChat.privacyStatementUrl); + const header = localize('setupHeader', "{0} is your AI pair programmer.", defaultChat.name); + const footer = localize({ key: 'setupFooter', comment: ['{Locked="]({0})"}'] }, "By proceeding you agree to our [privacy statement]({0}).", defaultChat.privacyStatementUrl); // Setup: Triggered (signed-out) Registry.as(ChatViewsWelcomeExtensions.ChatViewsWelcomeRegistry).register({ @@ -332,6 +331,7 @@ class ChatSetupRequestHelper { return await requestService.request({ type, url, + data: type === 'POST' ? JSON.stringify({}) : undefined, headers: { 'Authorization': `Bearer ${session.accessToken}` } @@ -500,6 +500,7 @@ class ChatSetupInstallAction extends Action2 { const contextKeyService = accessor.get(IContextKeyService); const viewsService = accessor.get(IViewsService); const chatAgentService = accessor.get(IChatAgentService); + const instantiationService = accessor.get(IInstantiationService); const signedIn = !!session; const setupInstallingContextKey = ChatContextKeys.Setup.installing.bindTo(contextKeyService); @@ -510,6 +511,8 @@ class ChatSetupInstallAction extends Action2 { setupInstallingContextKey.set(true); showChatView(viewsService); + await instantiationService.invokeFunction(accessor => ChatSetupRequestHelper.request(accessor, defaultChat.entitlementSkuAlternateUrl, 'POST', session, CancellationToken.None)); + await extensionsWorkbenchService.install(defaultChat.extensionId, { enable: true, isMachineScoped: false, From 65ff3309338352621bb325b5e9aa44360a07edc1 Mon Sep 17 00:00:00 2001 From: Peng Lyu Date: Thu, 21 Nov 2024 10:55:27 -0800 Subject: [PATCH 039/119] Notebook snapshot (cells and buffer) --- .../browser/services/notebookServiceImpl.ts | 31 ++++++++- .../common/model/notebookTextModel.ts | 65 +++++++++++++++---- .../contrib/notebook/common/notebookCommon.ts | 7 ++ .../notebook/common/notebookEditorModel.ts | 47 +------------- .../notebook/common/notebookService.ts | 2 + 5 files changed, 93 insertions(+), 59 deletions(-) diff --git a/src/vs/workbench/contrib/notebook/browser/services/notebookServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/services/notebookServiceImpl.ts index c57cb325bb36c..ed1dd4f33b642 100644 --- a/src/vs/workbench/contrib/notebook/browser/services/notebookServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/services/notebookServiceImpl.ts @@ -28,7 +28,7 @@ import { INotebookEditorOptions } from '../notebookBrowser.js'; import { NotebookDiffEditorInput } from '../../common/notebookDiffEditorInput.js'; import { NotebookCellTextModel } from '../../common/model/notebookCellTextModel.js'; import { NotebookTextModel } from '../../common/model/notebookTextModel.js'; -import { ACCESSIBLE_NOTEBOOK_DISPLAY_ORDER, CellUri, NotebookSetting, INotebookContributionData, INotebookExclusiveDocumentFilter, INotebookRendererInfo, INotebookTextModel, IOrderedMimeType, IOutputDto, MimeTypeDisplayOrder, NotebookEditorPriority, NotebookRendererMatch, NOTEBOOK_DISPLAY_ORDER, RENDERER_EQUIVALENT_EXTENSIONS, RENDERER_NOT_AVAILABLE, NotebookExtensionDescription, INotebookStaticPreloadInfo } from '../../common/notebookCommon.js'; +import { ACCESSIBLE_NOTEBOOK_DISPLAY_ORDER, CellUri, NotebookSetting, INotebookContributionData, INotebookExclusiveDocumentFilter, INotebookRendererInfo, INotebookTextModel, IOrderedMimeType, IOutputDto, MimeTypeDisplayOrder, NotebookEditorPriority, NotebookRendererMatch, NOTEBOOK_DISPLAY_ORDER, RENDERER_EQUIVALENT_EXTENSIONS, RENDERER_NOT_AVAILABLE, NotebookExtensionDescription, INotebookStaticPreloadInfo, NotebookData } from '../../common/notebookCommon.js'; import { NotebookEditorInput } from '../../common/notebookEditorInput.js'; import { INotebookEditorModelResolverService } from '../../common/notebookEditorModelResolverService.js'; import { NotebookOutputRendererInfo, NotebookStaticPreloadInfo as NotebookStaticPreloadInfo } from '../../common/notebookOutputRenderer.js'; @@ -42,9 +42,12 @@ import { IUriIdentityService } from '../../../../../platform/uriIdentity/common/ import { INotebookDocument, INotebookDocumentService } from '../../../../services/notebook/common/notebookDocumentService.js'; import { MergeEditorInput } from '../../../mergeEditor/browser/mergeEditorInput.js'; import type { EditorInputWithOptions, IResourceDiffEditorInput, IResourceMergeEditorInput } from '../../../../common/editor.js'; -import { streamToBuffer, VSBuffer, VSBufferReadableStream } from '../../../../../base/common/buffer.js'; +import { bufferToStream, streamToBuffer, VSBuffer, VSBufferReadableStream } from '../../../../../base/common/buffer.js'; import type { IEditorGroup } from '../../../../services/editor/common/editorGroupsService.js'; import { NotebookMultiDiffEditorInput } from '../diff/notebookMultiDiffEditorInput.js'; +import { SnapshotContext } from '../../../../services/workingCopy/common/fileWorkingCopy.js'; +import { CancellationToken } from '../../../../../base/common/cancellation.js'; +import { CancellationError } from '../../../../../base/common/errors.js'; export class NotebookProviderInfoStore extends Disposable { @@ -776,6 +779,30 @@ export class NotebookService extends Disposable implements INotebookService { return notebookModel; } + async createNotebookTextDocumentSnapshot(uri: URI, context: SnapshotContext, token: CancellationToken): Promise { + const model = this.getNotebookTextModel(uri); + + if (!model) { + throw new Error(`notebook for ${uri} doesn't exist`); + } + + const info = await this.withNotebookDataProvider(model.viewType); + + if (!(info instanceof SimpleNotebookProviderInfo)) { + throw new Error('CANNOT open file notebook with this provider'); + } + + const serializer = info.serializer; + const outputSizeLimit = this._configurationService.getValue(NotebookSetting.outputBackupSizeLimit) * 1024; + const data: NotebookData = model.createSnapshot({ context: context, outputSizeLimit: outputSizeLimit }); + const bytes = await serializer.notebookToData(data); + + if (token.isCancellationRequested) { + throw new CancellationError(); + } + return bufferToStream(bytes); + } + getNotebookTextModel(uri: URI): NotebookTextModel | undefined { return this._models.get(uri)?.model; } diff --git a/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts b/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts index c3dbee3af8949..bc039b1aca71c 100644 --- a/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts +++ b/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts @@ -3,29 +3,31 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Emitter, Event, PauseableEmitter } from '../../../../../base/common/event.js'; -import { Disposable, dispose, IDisposable } from '../../../../../base/common/lifecycle.js'; -import { URI } from '../../../../../base/common/uri.js'; -import { NotebookCellTextModel } from './notebookCellTextModel.js'; -import { INotebookTextModel, NotebookCellOutputsSplice, NotebookDocumentMetadata, NotebookCellMetadata, ICellEditOperation, CellEditType, CellUri, diff, NotebookCellsChangeType, ICellDto2, TransientOptions, NotebookTextModelChangedEvent, IOutputDto, ICellOutput, IOutputItemDto, ISelectionState, NullablePartialNotebookCellMetadata, NotebookCellInternalMetadata, NullablePartialNotebookCellInternalMetadata, NotebookTextModelWillAddRemoveEvent, NotebookCellTextModelSplice, ICell, NotebookCellCollapseState, NotebookCellDefaultCollapseConfig, CellKind, NotebookCellExecutionState } from '../notebookCommon.js'; -import { IUndoRedoService, UndoRedoElementType, IUndoRedoElement, IResourceUndoRedoElement, UndoRedoGroup, IWorkspaceUndoRedoElement } from '../../../../../platform/undoRedo/common/undoRedo.js'; -import { MoveCellEdit, SpliceCellsEdit, CellMetadataEdit } from './cellEdit.js'; import { ISequence, LcsDiff } from '../../../../../base/common/diff/diff.js'; +import { Emitter, Event, PauseableEmitter } from '../../../../../base/common/event.js'; import { hash } from '../../../../../base/common/hash.js'; -import { NotebookCellOutputTextModel } from './notebookCellOutputTextModel.js'; -import { IModelService } from '../../../../../editor/common/services/model.js'; +import { Disposable, dispose, IDisposable } from '../../../../../base/common/lifecycle.js'; import { Schemas } from '../../../../../base/common/network.js'; +import { filter } from '../../../../../base/common/objects.js'; import { isEqual } from '../../../../../base/common/resources.js'; -import { ILanguageService } from '../../../../../editor/common/languages/language.js'; -import { FindMatch, ITextModel } from '../../../../../editor/common/model.js'; -import { TextModel } from '../../../../../editor/common/model/textModel.js'; import { isDefined } from '../../../../../base/common/types.js'; -import { ILanguageDetectionService } from '../../../../services/languageDetection/common/languageDetectionWorkerService.js'; +import { URI } from '../../../../../base/common/uri.js'; import { Position } from '../../../../../editor/common/core/position.js'; import { Range } from '../../../../../editor/common/core/range.js'; +import { ILanguageService } from '../../../../../editor/common/languages/language.js'; +import { FindMatch, ITextModel } from '../../../../../editor/common/model.js'; +import { TextModel } from '../../../../../editor/common/model/textModel.js'; import { SearchParams } from '../../../../../editor/common/model/textModelSearch.js'; +import { IModelService } from '../../../../../editor/common/services/model.js'; import { IModelContentChangedEvent } from '../../../../../editor/common/textModelEvents.js'; +import { IResourceUndoRedoElement, IUndoRedoElement, IUndoRedoService, IWorkspaceUndoRedoElement, UndoRedoElementType, UndoRedoGroup } from '../../../../../platform/undoRedo/common/undoRedo.js'; +import { ILanguageDetectionService } from '../../../../services/languageDetection/common/languageDetectionWorkerService.js'; +import { SnapshotContext } from '../../../../services/workingCopy/common/fileWorkingCopy.js'; +import { CellEditType, CellKind, CellUri, diff, ICell, ICellDto2, ICellEditOperation, ICellOutput, INotebookSnapshotOptions, INotebookTextModel, IOutputDto, IOutputItemDto, ISelectionState, NotebookCellCollapseState, NotebookCellDefaultCollapseConfig, NotebookCellExecutionState, NotebookCellInternalMetadata, NotebookCellMetadata, NotebookCellOutputsSplice, NotebookCellsChangeType, NotebookCellTextModelSplice, NotebookData, NotebookDocumentMetadata, NotebookTextModelChangedEvent, NotebookTextModelWillAddRemoveEvent, NullablePartialNotebookCellInternalMetadata, NullablePartialNotebookCellMetadata, TransientOptions } from '../notebookCommon.js'; import { INotebookExecutionStateService } from '../notebookExecutionStateService.js'; +import { CellMetadataEdit, MoveCellEdit, SpliceCellsEdit } from './cellEdit.js'; +import { NotebookCellOutputTextModel } from './notebookCellOutputTextModel.js'; +import { NotebookCellTextModel } from './notebookCellTextModel.js'; class StackOperation implements IWorkspaceUndoRedoElement { type: UndoRedoElementType.Workspace; @@ -441,6 +443,43 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel ); } + createSnapshot(options: INotebookSnapshotOptions): NotebookData { + const data: NotebookData = { + metadata: filter(this.metadata, key => !this.transientOptions.transientDocumentMetadata[key]), + cells: [], + }; + + let outputSize = 0; + for (const cell of this.cells) { + const cellData: ICellDto2 = { + cellKind: cell.cellKind, + language: cell.language, + mime: cell.mime, + source: cell.getValue(), + outputs: [], + internalMetadata: cell.internalMetadata + }; + + if (options.context === SnapshotContext.Backup && options.outputSizeLimit > 0) { + cell.outputs.forEach(output => { + output.outputs.forEach(item => { + outputSize += item.data.byteLength; + }); + }); + if (outputSize > options.outputSizeLimit) { + throw new Error('Notebook too large to backup'); + } + } + + cellData.outputs = !this.transientOptions.transientOutputs ? cell.outputs : []; + cellData.metadata = filter(cell.metadata, key => !this.transientOptions.transientCellMetadata[key]); + + data.cells.push(cellData); + } + + return data; + } + static computeEdits(model: NotebookTextModel, cells: ICellDto2[], executingHandles: number[] = []): ICellEditOperation[] { const edits: ICellEditOperation[] = []; const isExecuting = (cell: NotebookCellTextModel) => executingHandles.includes(cell.handle); diff --git a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts index 9abec0e547a7e..25e94b8561993 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts @@ -36,6 +36,7 @@ import { ICellRange } from './notebookRange.js'; import { RegisteredEditorPriority } from '../../../services/editor/common/editorResolverService.js'; import { generateMetadataUri, generate as generateUri, parseMetadataUri, parse as parseUri } from '../../../services/notebook/common/notebookDocumentService.js'; import { IWorkingCopyBackupMeta, IWorkingCopySaveEvent } from '../../../services/workingCopy/common/workingCopy.js'; +import { SnapshotContext } from '../../../services/workingCopy/common/fileWorkingCopy.js'; export const NOTEBOOK_EDITOR_ID = 'workbench.editor.notebook'; export const NOTEBOOK_DIFF_EDITOR_ID = 'workbench.editor.notebookTextDiffEditor'; @@ -276,6 +277,11 @@ export interface ICell { onDidChangeInternalMetadata: Event; } +export interface INotebookSnapshotOptions { + context: SnapshotContext; + outputSizeLimit: number; +} + export interface INotebookTextModel extends INotebookTextModelLike { readonly notebookType: string; readonly viewType: string; @@ -286,6 +292,7 @@ export interface INotebookTextModel extends INotebookTextModelLike { readonly length: number; readonly cells: readonly ICell[]; reset(cells: ICellDto2[], metadata: NotebookDocumentMetadata, transientOptions: TransientOptions): void; + createSnapshot(options: INotebookSnapshotOptions): NotebookData; applyEdits(rawEdits: ICellEditOperation[], synchronous: boolean, beginSelectionState: ISelectionState | undefined, endSelectionsComputer: () => ISelectionState | undefined, undoRedoGroup: UndoRedoGroup | undefined, computeUndoRedo?: boolean): boolean; onDidChangeContent: Event; onWillDispose: Event; diff --git a/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts b/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts index c48d382960a0d..bab8c66f9425b 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts @@ -3,14 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { VSBufferReadableStream, bufferToStream, streamToBuffer } from '../../../../base/common/buffer.js'; +import { VSBufferReadableStream, streamToBuffer } from '../../../../base/common/buffer.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; import { CancellationError } from '../../../../base/common/errors.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { IMarkdownString } from '../../../../base/common/htmlContent.js'; import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js'; import { Schemas } from '../../../../base/common/network.js'; -import { filter } from '../../../../base/common/objects.js'; import { assertType } from '../../../../base/common/types.js'; import { URI } from '../../../../base/common/uri.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; @@ -19,7 +18,7 @@ import { ITelemetryService } from '../../../../platform/telemetry/common/telemet import { IRevertOptions, ISaveOptions, IUntypedEditorInput } from '../../../common/editor.js'; import { EditorModel } from '../../../common/editor/editorModel.js'; import { NotebookTextModel } from './model/notebookTextModel.js'; -import { ICellDto2, INotebookEditorModel, INotebookLoadOptions, IResolvedNotebookEditorModel, NotebookCellsChangeType, NotebookData, NotebookSetting } from './notebookCommon.js'; +import { INotebookEditorModel, INotebookLoadOptions, IResolvedNotebookEditorModel, NotebookCellsChangeType, NotebookSetting } from './notebookCommon.js'; import { INotebookLoggingService } from './notebookLoggingService.js'; import { INotebookSerializer, INotebookService, SimpleNotebookProviderInfo } from './notebookService.js'; import { IFilesConfigurationService } from '../../../services/filesConfiguration/common/filesConfigurationService.js'; @@ -294,47 +293,7 @@ export class NotebookFileWorkingCopyModel extends Disposable implements IStoredF } async snapshot(context: SnapshotContext, token: CancellationToken): Promise { - const serializer = await this.getNotebookSerializer(); - - const data: NotebookData = { - metadata: filter(this._notebookModel.metadata, key => !serializer.options.transientDocumentMetadata[key]), - cells: [], - }; - - let outputSize = 0; - for (const cell of this._notebookModel.cells) { - const cellData: ICellDto2 = { - cellKind: cell.cellKind, - language: cell.language, - mime: cell.mime, - source: cell.getValue(), - outputs: [], - internalMetadata: cell.internalMetadata - }; - - const outputSizeLimit = this._configurationService.getValue(NotebookSetting.outputBackupSizeLimit) * 1024; - if (context === SnapshotContext.Backup && outputSizeLimit > 0) { - cell.outputs.forEach(output => { - output.outputs.forEach(item => { - outputSize += item.data.byteLength; - }); - }); - if (outputSize > outputSizeLimit) { - throw new Error('Notebook too large to backup'); - } - } - - cellData.outputs = !serializer.options.transientOutputs ? cell.outputs : []; - cellData.metadata = filter(cell.metadata, key => !serializer.options.transientCellMetadata[key]); - - data.cells.push(cellData); - } - - const bytes = await serializer.notebookToData(data); - if (token.isCancellationRequested) { - throw new CancellationError(); - } - return bufferToStream(bytes); + return this._notebookService.createNotebookTextDocumentSnapshot(this._notebookModel.uri, context, token); } async update(stream: VSBufferReadableStream, token: CancellationToken): Promise { diff --git a/src/vs/workbench/contrib/notebook/common/notebookService.ts b/src/vs/workbench/contrib/notebook/common/notebookService.ts index aa4af44726e2a..553e38a69a9fe 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookService.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookService.ts @@ -18,6 +18,7 @@ import { IFileStatWithMetadata, IWriteFileOptions } from '../../../../platform/f import { ITextQuery } from '../../../services/search/common/search.js'; import { NotebookPriorityInfo } from '../../search/common/search.js'; import { INotebookFileMatchNoModel } from '../../search/common/searchNotebookHelpers.js'; +import { SnapshotContext } from '../../../services/workingCopy/common/fileWorkingCopy.js'; export const INotebookService = createDecorator('notebookService'); @@ -80,6 +81,7 @@ export interface INotebookService { saveMimeDisplayOrder(target: ConfigurationTarget): void; createNotebookTextModel(viewType: string, uri: URI, stream?: VSBufferReadableStream): Promise; + createNotebookTextDocumentSnapshot(uri: URI, context: SnapshotContext, token: CancellationToken): Promise; getNotebookTextModel(uri: URI): NotebookTextModel | undefined; getNotebookTextModels(): Iterable; listNotebookDocuments(): readonly NotebookTextModel[]; From 46ee2f93d637f488044df9688d25828c8ac26aef Mon Sep 17 00:00:00 2001 From: Peng Lyu Date: Thu, 21 Nov 2024 11:02:39 -0800 Subject: [PATCH 040/119] RestoreSnapshot --- .../browser/services/notebookServiceImpl.ts | 22 +++++++++++++++++++ .../common/model/notebookTextModel.ts | 4 ++++ .../contrib/notebook/common/notebookCommon.ts | 1 + .../notebook/common/notebookService.ts | 1 + 4 files changed, 28 insertions(+) diff --git a/src/vs/workbench/contrib/notebook/browser/services/notebookServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/services/notebookServiceImpl.ts index ed1dd4f33b642..ed2a166a20132 100644 --- a/src/vs/workbench/contrib/notebook/browser/services/notebookServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/services/notebookServiceImpl.ts @@ -803,6 +803,28 @@ export class NotebookService extends Disposable implements INotebookService { return bufferToStream(bytes); } + async restoreNotebookTextModelFromSnapshot(uri: URI, viewType: string, snapshot: VSBufferReadableStream): Promise { + const model = this.getNotebookTextModel(uri); + + if (!model) { + throw new Error(`notebook for ${uri} doesn't exist`); + } + + const info = await this.withNotebookDataProvider(model.viewType); + + if (!(info instanceof SimpleNotebookProviderInfo)) { + throw new Error('CANNOT open file notebook with this provider'); + } + + const serializer = info.serializer; + + const bytes = await streamToBuffer(snapshot); + const data = await info.serializer.dataToNotebook(bytes); + model.restoreSnapshot(data, serializer.options); + + return model; + } + getNotebookTextModel(uri: URI): NotebookTextModel | undefined { return this._models.get(uri)?.model; } diff --git a/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts b/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts index bc039b1aca71c..eda48391ecf59 100644 --- a/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts +++ b/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts @@ -480,6 +480,10 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel return data; } + restoreSnapshot(snapshot: NotebookData, transientOptions?: TransientOptions): void { + this.reset(snapshot.cells, snapshot.metadata, transientOptions ?? this.transientOptions); + } + static computeEdits(model: NotebookTextModel, cells: ICellDto2[], executingHandles: number[] = []): ICellEditOperation[] { const edits: ICellEditOperation[] = []; const isExecuting = (cell: NotebookCellTextModel) => executingHandles.includes(cell.handle); diff --git a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts index 25e94b8561993..26ea447e9b6cd 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts @@ -293,6 +293,7 @@ export interface INotebookTextModel extends INotebookTextModelLike { readonly cells: readonly ICell[]; reset(cells: ICellDto2[], metadata: NotebookDocumentMetadata, transientOptions: TransientOptions): void; createSnapshot(options: INotebookSnapshotOptions): NotebookData; + restoreSnapshot(snapshot: NotebookData, transientOptions?: TransientOptions): void; applyEdits(rawEdits: ICellEditOperation[], synchronous: boolean, beginSelectionState: ISelectionState | undefined, endSelectionsComputer: () => ISelectionState | undefined, undoRedoGroup: UndoRedoGroup | undefined, computeUndoRedo?: boolean): boolean; onDidChangeContent: Event; onWillDispose: Event; diff --git a/src/vs/workbench/contrib/notebook/common/notebookService.ts b/src/vs/workbench/contrib/notebook/common/notebookService.ts index 553e38a69a9fe..449a985c6882c 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookService.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookService.ts @@ -82,6 +82,7 @@ export interface INotebookService { createNotebookTextModel(viewType: string, uri: URI, stream?: VSBufferReadableStream): Promise; createNotebookTextDocumentSnapshot(uri: URI, context: SnapshotContext, token: CancellationToken): Promise; + restoreNotebookTextModelFromSnapshot(uri: URI, viewType: string, snapshot: VSBufferReadableStream): Promise; getNotebookTextModel(uri: URI): NotebookTextModel | undefined; getNotebookTextModels(): Iterable; listNotebookDocuments(): readonly NotebookTextModel[]; From 2db88999994a957165ac38b63ff647b8090c1b51 Mon Sep 17 00:00:00 2001 From: Peng Lyu Date: Thu, 21 Nov 2024 11:38:28 -0800 Subject: [PATCH 041/119] fix tests --- .../browser/services/notebookServiceImpl.ts | 2 +- .../notebook/common/model/notebookTextModel.ts | 7 ++++--- .../contrib/notebook/common/notebookCommon.ts | 1 + .../test/browser/notebookEditorModel.test.ts | 16 +++++++++++++--- 4 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/vs/workbench/contrib/notebook/browser/services/notebookServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/services/notebookServiceImpl.ts index ed2a166a20132..80d1a7ad448fe 100644 --- a/src/vs/workbench/contrib/notebook/browser/services/notebookServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/services/notebookServiceImpl.ts @@ -794,7 +794,7 @@ export class NotebookService extends Disposable implements INotebookService { const serializer = info.serializer; const outputSizeLimit = this._configurationService.getValue(NotebookSetting.outputBackupSizeLimit) * 1024; - const data: NotebookData = model.createSnapshot({ context: context, outputSizeLimit: outputSizeLimit }); + const data: NotebookData = model.createSnapshot({ context: context, outputSizeLimit: outputSizeLimit, transientOptions: serializer.options }); const bytes = await serializer.notebookToData(data); if (token.isCancellationRequested) { diff --git a/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts b/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts index eda48391ecf59..a7f1c786742ab 100644 --- a/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts +++ b/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts @@ -444,8 +444,9 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel } createSnapshot(options: INotebookSnapshotOptions): NotebookData { + const transientOptions = options.transientOptions ?? this.transientOptions; const data: NotebookData = { - metadata: filter(this.metadata, key => !this.transientOptions.transientDocumentMetadata[key]), + metadata: filter(this.metadata, key => !transientOptions.transientDocumentMetadata[key]), cells: [], }; @@ -471,8 +472,8 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel } } - cellData.outputs = !this.transientOptions.transientOutputs ? cell.outputs : []; - cellData.metadata = filter(cell.metadata, key => !this.transientOptions.transientCellMetadata[key]); + cellData.outputs = !transientOptions.transientOutputs ? cell.outputs : []; + cellData.metadata = filter(cell.metadata, key => !transientOptions.transientCellMetadata[key]); data.cells.push(cellData); } diff --git a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts index 26ea447e9b6cd..300b9209edc4e 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts @@ -280,6 +280,7 @@ export interface ICell { export interface INotebookSnapshotOptions { context: SnapshotContext; outputSizeLimit: number; + transientOptions?: TransientOptions; } export interface INotebookTextModel extends INotebookTextModelLike { diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookEditorModel.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookEditorModel.test.ts index 48f4e9a231a80..5ab182642248f 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookEditorModel.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookEditorModel.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; -import { VSBuffer } from '../../../../../base/common/buffer.js'; +import { bufferToStream, VSBuffer, VSBufferReadableStream } from '../../../../../base/common/buffer.js'; import { CancellationToken } from '../../../../../base/common/cancellation.js'; import { DisposableStore } from '../../../../../base/common/lifecycle.js'; import { Mimes } from '../../../../../base/common/mime.js'; @@ -246,7 +246,8 @@ suite('NotebookFileWorkingCopyModel', function () { assert.strictEqual(notebook.cells[0].metadata!.bar, undefined); return VSBuffer.fromString(''); } - } + }, + configurationService ), configurationService, telemetryService, @@ -309,7 +310,7 @@ suite('NotebookFileWorkingCopyModel', function () { }); }); -function mockNotebookService(notebook: NotebookTextModel, notebookSerializer: Promise | INotebookSerializer) { +function mockNotebookService(notebook: NotebookTextModel, notebookSerializer: Promise | INotebookSerializer, configurationService: TestConfigurationService = new TestConfigurationService()): INotebookService { return new class extends mock() { private serializer: INotebookSerializer | undefined = undefined; override async withNotebookDataProvider(viewType: string): Promise { @@ -336,5 +337,14 @@ function mockNotebookService(notebook: NotebookTextModel, notebookSerializer: Pr } ); } + override async createNotebookTextDocumentSnapshot(uri: URI, context: SnapshotContext, token: CancellationToken): Promise { + const info = await this.withNotebookDataProvider(notebook.viewType); + const serializer = info.serializer; + const outputSizeLimit = configurationService.getValue(NotebookSetting.outputBackupSizeLimit) ?? 1024; + const data: NotebookData = notebook.createSnapshot({ context: context, outputSizeLimit: outputSizeLimit, transientOptions: serializer.options }); + const bytes = await serializer.notebookToData(data); + + return bufferToStream(bytes); + } }; } From 250a1752227d376a12bb994a5520bcdf750e2e3b Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Thu, 21 Nov 2024 19:46:47 +0100 Subject: [PATCH 042/119] edits: more simplifications (#234377) * edits: more simplifications * remove unused imports --- .../browser/chatEditing/chatEditingService.ts | 74 +++++-------------- .../contrib/chat/browser/chatWidget.ts | 5 -- 2 files changed, 20 insertions(+), 59 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingService.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingService.ts index a90172eacd107..a4255e29f0d33 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingService.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingService.ts @@ -19,18 +19,14 @@ import { TextEdit } from '../../../../../editor/common/languages.js'; import { ITextModelService } from '../../../../../editor/common/services/resolverService.js'; import { localize, localize2 } from '../../../../../nls.js'; import { IContextKey, IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; -import { EditorActivation } from '../../../../../platform/editor/common/editor.js'; import { IFileService } from '../../../../../platform/files/common/files.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { bindContextKey } from '../../../../../platform/observable/common/platformObservableUtils.js'; import { IProgressService, ProgressLocation } from '../../../../../platform/progress/common/progress.js'; -import { EditorInput } from '../../../../common/editor/editorInput.js'; import { IWorkbenchAssignmentService } from '../../../../services/assignment/common/assignmentService.js'; import { IDecorationData, IDecorationsProvider, IDecorationsService } from '../../../../services/decorations/common/decorations.js'; -import { IEditorGroup, IEditorGroupsService } from '../../../../services/editor/common/editorGroupsService.js'; import { IEditorService } from '../../../../services/editor/common/editorService.js'; import { ILifecycleService } from '../../../../services/lifecycle/common/lifecycle.js'; -import { MultiDiffEditor } from '../../../multiDiffEditor/browser/multiDiffEditor.js'; import { IMultiDiffSourceResolver, IMultiDiffSourceResolverService, IResolvedMultiDiffSource, MultiDiffEditorItem } from '../../../multiDiffEditor/browser/multiDiffSourceResolverService.js'; import { ChatAgentLocation, IChatAgentService } from '../../common/chatAgents.js'; import { ChatContextKeys } from '../../common/chatContextKeys.js'; @@ -79,7 +75,6 @@ export class ChatEditingService extends Disposable implements IChatEditingServic private _chatRelatedFilesProviders = new Map(); constructor( - @IEditorGroupsService private readonly _editorGroupsService: IEditorGroupsService, @IInstantiationService private readonly _instantiationService: IInstantiationService, @IMultiDiffSourceResolverService multiDiffSourceResolverService: IMultiDiffSourceResolverService, @ITextModelService textModelService: ITextModelService, @@ -204,10 +199,11 @@ export class ChatEditingService extends Disposable implements IChatEditingServic this._currentSessionDisposables.clear(); + const session = this._instantiationService.createInstance(ChatEditingSession, chatSessionId, this._editingSessionFileLimitPromise); + // listen for completed responses, run the code mapper and apply the edits to this edit session - this._currentSessionDisposables.add(this.installAutoApplyObserver(chatSessionId)); + this._currentSessionDisposables.add(this.installAutoApplyObserver(session)); - const session = this._instantiationService.createInstance(ChatEditingSession, chatSessionId, this._editingSessionFileLimitPromise); await session.restoreState(); this._currentSessionDisposables.add(session.onDidDispose(() => { @@ -233,11 +229,11 @@ export class ChatEditingService extends Disposable implements IChatEditingServic await this._currentSessionObs.get()?.restoreSnapshot(requestId); } - private installAutoApplyObserver(sessionId: string): IDisposable { + private installAutoApplyObserver(session: ChatEditingSession): IDisposable { - const chatModel = this._chatService.getSession(sessionId); + const chatModel = this._chatService.getSession(session.chatSessionId); if (!chatModel) { - throw new Error(`Edit session was created for a non-existing chat session: ${sessionId}`); + throw new Error(`Edit session was created for a non-existing chat session: ${session.chatSessionId}`); } const observerDisposables = new DisposableStore(); @@ -250,7 +246,7 @@ export class ChatEditingService extends Disposable implements IChatEditingServic const onResponseComplete = (responseModel: IChatResponseModel) => { if (responseModel.result?.errorDetails && !responseModel.result.errorDetails.responseIsIncomplete) { // Roll back everything - this.restoreSnapshot(responseModel.requestId); + session.restoreSnapshot(responseModel.requestId); this._applyingChatEditsFailedContextKey.set(true); } @@ -293,7 +289,7 @@ export class ChatEditingService extends Disposable implements IChatEditingServic await editsPromise; - editsPromise = this._continueEditingSession(async (builder, token) => { + editsPromise = this._continueEditingSession(session, async (builder, token) => { for await (const item of editsSource!.asyncIterable) { if (token.isCancellationRequested) { break; @@ -304,7 +300,7 @@ export class ChatEditingService extends Disposable implements IChatEditingServic builder.textEdits(item.uri, group, isLastGroup && (item.done ?? false), responseModel); } } - }, { silent: true }).finally(() => { + }).finally(() => { editsPromise = undefined; }); } @@ -314,6 +310,7 @@ export class ChatEditingService extends Disposable implements IChatEditingServic observerDisposables.add(chatModel.onDidChange(async e => { if (e.kind === 'addRequest') { + session.createSnapshot(e.request.id); this._applyingChatEditsFailedContextKey.set(false); const responseModel = e.request.response; if (responseModel) { @@ -338,27 +335,11 @@ export class ChatEditingService extends Disposable implements IChatEditingServic return observerDisposables; } - private async _continueEditingSession(builder: (stream: IChatEditingSessionStream, token: CancellationToken) => Promise, options?: { silent?: boolean }): Promise { - const session = this._currentSessionObs.get(); - if (!session) { - throw new BugIndicatingError('Cannot continue missing session'); - } - + private async _continueEditingSession(session: ChatEditingSession, builder: (stream: IChatEditingSessionStream, token: CancellationToken) => Promise): Promise { if (session.state.get() === ChatEditingSessionState.StreamingEdits) { throw new BugIndicatingError('Cannot continue session that is still streaming'); } - let editorPane: MultiDiffEditor | undefined; - if (!options?.silent && session.isVisible) { - const groupedEditors = this._findGroupedEditors(); - if (groupedEditors.length !== 1) { - throw new Error(`Unexpected number of editors: ${groupedEditors.length}`); - } - const [group, editor] = groupedEditors[0]; - - editorPane = await group.openEditor(editor, { pinned: true, activation: EditorActivation.ACTIVATE }) as MultiDiffEditor | undefined; - } - const stream: IChatEditingSessionStream = { textEdits: (resource: URI, textEdits: TextEdit[], isDone: boolean, responseModel: IChatResponseModel) => { session.acceptTextEdits(resource, textEdits, isDone, responseModel); @@ -368,18 +349,15 @@ export class ChatEditingService extends Disposable implements IChatEditingServic const cancellationTokenSource = new CancellationTokenSource(); this._currentAutoApplyOperationObs.set(cancellationTokenSource, undefined); try { - if (editorPane) { - await editorPane?.showWhile(builder(stream, cancellationTokenSource.token)); - } else { - await this._progressService.withProgress({ - location: ProgressLocation.Window, - title: localize2('chatEditing.startingSession', 'Generating edits...').value, - }, async () => { - await builder(stream, cancellationTokenSource.token); - }, - () => cancellationTokenSource.cancel() - ); - } + await this._progressService.withProgress({ + location: ProgressLocation.Window, + title: localize2('chatEditing.startingSession', 'Generating edits...').value, + }, async () => { + await builder(stream, cancellationTokenSource.token); + }, + () => cancellationTokenSource.cancel() + ); + } finally { cancellationTokenSource.dispose(); this._currentAutoApplyOperationObs.set(null, undefined); @@ -387,18 +365,6 @@ export class ChatEditingService extends Disposable implements IChatEditingServic } } - private _findGroupedEditors() { - const editors: [IEditorGroup, EditorInput][] = []; - for (const group of this._editorGroupsService.groups) { - for (const editor of group.editors) { - if (editor.resource?.scheme === ChatEditingMultiDiffSourceResolver.scheme) { - editors.push([group, editor]); - } - } - } - return editors; - } - hasRelatedFilesProviders(): boolean { return this._chatRelatedFilesProviders.size > 0; } diff --git a/src/vs/workbench/contrib/chat/browser/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/chatWidget.ts index 3b0aaa58dc859..316f2a724d6bd 100644 --- a/src/vs/workbench/contrib/chat/browser/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatWidget.ts @@ -889,11 +889,6 @@ export class ChatWidget extends Disposable implements IChatWidget { if (e.kind === 'setAgent') { this._onDidChangeAgent.fire({ agent: e.agent, slashCommand: e.command }); } - if (e.kind === 'addRequest') { - if (this._location.location === ChatAgentLocation.EditingSession) { - this.chatEditingService.createSnapshot(e.request.id); - } - } })); if (this.tree && this.visible) { From 201e98f942e58d8c997bfee4d132e2da3d9a15ec Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt Date: Thu, 21 Nov 2024 10:58:49 -0800 Subject: [PATCH 043/119] `handle: string` to `nativeHandle: UInt8Array` based on feedback (#234378) Feedback in https://github.com/microsoft/vscode/issues/229431 --- .../src/node/authProvider.ts | 4 ++-- src/vs/platform/window/common/window.ts | 3 ++- .../platform/windows/electron-main/windowImpl.ts | 3 ++- src/vs/workbench/api/common/extHost.api.impl.ts | 4 ++-- src/vs/workbench/api/common/extHostWindow.ts | 15 ++++++++++++++- .../electron-sandbox/environmentService.ts | 3 ++- .../electron-sandbox/localProcessExtensionHost.ts | 4 ++-- .../vscode.proposed.nativeWindowHandle.d.ts | 6 +++--- 8 files changed, 29 insertions(+), 13 deletions(-) diff --git a/extensions/microsoft-authentication/src/node/authProvider.ts b/extensions/microsoft-authentication/src/node/authProvider.ts index e5d7440f6f3c7..f6c771e4f9ae4 100644 --- a/extensions/microsoft-authentication/src/node/authProvider.ts +++ b/extensions/microsoft-authentication/src/node/authProvider.ts @@ -190,7 +190,7 @@ export class MsalAuthProvider implements AuthenticationProvider { let result: AuthenticationResult | undefined; try { - const windowHandle = env.handle ? Buffer.from(env.handle, 'base64') : undefined; + const windowHandle = env.nativeHandle ? Buffer.from(env.nativeHandle) : undefined; result = await cachedPca.acquireTokenInteractive({ openBrowser: async (url: string) => { await env.openExternal(Uri.parse(url)); }, scopes: scopeData.scopesToSend, @@ -232,7 +232,7 @@ export class MsalAuthProvider implements AuthenticationProvider { // The user wants to try the loopback client or we got an error likely due to spinning up the server const loopbackClient = new UriHandlerLoopbackClient(this._uriHandler, redirectUri, this._logger); try { - const windowHandle = env.handle ? Buffer.from(env.handle) : undefined; + const windowHandle = env.nativeHandle ? Buffer.from(env.nativeHandle) : undefined; result = await cachedPca.acquireTokenInteractive({ openBrowser: (url: string) => loopbackClient.openBrowser(url), scopes: scopeData.scopesToSend, diff --git a/src/vs/platform/window/common/window.ts b/src/vs/platform/window/common/window.ts index 86fb83e422486..d547c37bf50e4 100644 --- a/src/vs/platform/window/common/window.ts +++ b/src/vs/platform/window/common/window.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { VSBuffer } from '../../../base/common/buffer.js'; import { IStringDictionary } from '../../../base/common/collections.js'; import { PerformanceMark } from '../../../base/common/performance.js'; import { isLinux, isMacintosh, isNative, isWeb } from '../../../base/common/platform.js'; @@ -355,7 +356,7 @@ export interface IOSConfiguration { export interface INativeWindowConfiguration extends IWindowConfiguration, NativeParsedArgs, ISandboxConfiguration { mainPid: number; - handle?: string; + handle?: VSBuffer; machineId: string; sqmId: string; diff --git a/src/vs/platform/windows/electron-main/windowImpl.ts b/src/vs/platform/windows/electron-main/windowImpl.ts index 6a3ecb6a7e1aa..3a9132dd3cd96 100644 --- a/src/vs/platform/windows/electron-main/windowImpl.ts +++ b/src/vs/platform/windows/electron-main/windowImpl.ts @@ -43,6 +43,7 @@ import { IStateService } from '../../state/node/state.js'; import { IUserDataProfilesMainService } from '../../userDataProfile/electron-main/userDataProfile.js'; import { ILoggerMainService } from '../../log/electron-main/loggerService.js'; import { IInstantiationService } from '../../instantiation/common/instantiation.js'; +import { VSBuffer } from '../../../base/common/buffer.js'; export interface IWindowCreationOptions { readonly state: IWindowState; @@ -1093,7 +1094,7 @@ export class CodeWindow extends BaseWindow implements ICodeWindow { // Update window related properties try { - configuration.handle = this._win.getNativeWindowHandle().toString('base64'); + configuration.handle = VSBuffer.wrap(this._win.getNativeWindowHandle()); } catch (error) { this.logService.error(`Error getting native window handle: ${error}`); } diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 8d2a972f62bd0..672848e05fe61 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -437,9 +437,9 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I checkProposedApiEnabled(extension, 'resolvers'); return initData.commit; }, - get handle(): string | undefined { + get nativeHandle(): Uint8Array | undefined { checkProposedApiEnabled(extension, 'nativeWindowHandle'); - return initData.handle; + return extHostWindow.nativeHandle; } }; if (!initData.environment.extensionTestsLocationURI) { diff --git a/src/vs/workbench/api/common/extHostWindow.ts b/src/vs/workbench/api/common/extHostWindow.ts index 6f74b721b8a88..e75364dc089cb 100644 --- a/src/vs/workbench/api/common/extHostWindow.ts +++ b/src/vs/workbench/api/common/extHostWindow.ts @@ -11,6 +11,8 @@ import { createDecorator } from '../../../platform/instantiation/common/instanti import { IExtHostRpcService } from './extHostRpcService.js'; import { WindowState } from 'vscode'; import { ExtHostWindowShape, IOpenUriOptions, MainContext, MainThreadWindowShape } from './extHost.protocol.js'; +import { IExtHostInitDataService } from './extHostInitDataService.js'; +import { decodeBase64 } from '../../../base/common/buffer.js'; export class ExtHostWindow implements ExtHostWindowShape { @@ -24,6 +26,7 @@ export class ExtHostWindow implements ExtHostWindowShape { private readonly _onDidChangeWindowState = new Emitter(); readonly onDidChangeWindowState: Event = this._onDidChangeWindowState.event; + private _nativeHandle: Uint8Array | undefined; private _state = ExtHostWindow.InitialState; getState(): WindowState { @@ -40,7 +43,13 @@ export class ExtHostWindow implements ExtHostWindowShape { }; } - constructor(@IExtHostRpcService extHostRpc: IExtHostRpcService) { + constructor( + @IExtHostInitDataService initData: IExtHostInitDataService, + @IExtHostRpcService extHostRpc: IExtHostRpcService + ) { + if (initData.handle) { + this._nativeHandle = decodeBase64(initData.handle).buffer; + } this._proxy = extHostRpc.getProxy(MainContext.MainThreadWindow); this._proxy.$getInitialState().then(({ isFocused, isActive }) => { this.onDidChangeWindowProperty('focused', isFocused); @@ -48,6 +57,10 @@ export class ExtHostWindow implements ExtHostWindowShape { }); } + get nativeHandle(): Uint8Array | undefined { + return this._nativeHandle; + } + $onDidChangeWindowFocus(value: boolean) { this.onDidChangeWindowProperty('focused', value); } diff --git a/src/vs/workbench/services/environment/electron-sandbox/environmentService.ts b/src/vs/workbench/services/environment/electron-sandbox/environmentService.ts index aebabc002032a..1f25e5b8aee74 100644 --- a/src/vs/workbench/services/environment/electron-sandbox/environmentService.ts +++ b/src/vs/workbench/services/environment/electron-sandbox/environmentService.ts @@ -14,6 +14,7 @@ import { URI } from '../../../../base/common/uri.js'; import { Schemas } from '../../../../base/common/network.js'; import { IProductService } from '../../../../platform/product/common/productService.js'; import { joinPath } from '../../../../base/common/resources.js'; +import { VSBuffer } from '../../../../base/common/buffer.js'; export const INativeWorkbenchEnvironmentService = refineServiceDecorator(IEnvironmentService); @@ -26,7 +27,7 @@ export interface INativeWorkbenchEnvironmentService extends IBrowserWorkbenchEnv // --- Window readonly window: { id: number; - handle?: string; + handle?: VSBuffer; colorScheme: IColorScheme; maximized?: boolean; accessibilitySupport?: boolean; diff --git a/src/vs/workbench/services/extensions/electron-sandbox/localProcessExtensionHost.ts b/src/vs/workbench/services/extensions/electron-sandbox/localProcessExtensionHost.ts index c815dbddf6ba8..ff0ebd397e546 100644 --- a/src/vs/workbench/services/extensions/electron-sandbox/localProcessExtensionHost.ts +++ b/src/vs/workbench/services/extensions/electron-sandbox/localProcessExtensionHost.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { timeout } from '../../../../base/common/async.js'; -import { VSBuffer } from '../../../../base/common/buffer.js'; +import { encodeBase64, VSBuffer } from '../../../../base/common/buffer.js'; import { CancellationError } from '../../../../base/common/errors.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js'; @@ -512,7 +512,7 @@ export class NativeLocalProcessExtensionHost implements IExtensionHost { logsLocation: this._environmentService.extHostLogsPath, autoStart: (this.startup === ExtensionHostStartup.EagerAutoStart), uiKind: UIKind.Desktop, - handle: this._environmentService.window.handle + handle: this._environmentService.window.handle ? encodeBase64(this._environmentService.window.handle) : undefined }; } diff --git a/src/vscode-dts/vscode.proposed.nativeWindowHandle.d.ts b/src/vscode-dts/vscode.proposed.nativeWindowHandle.d.ts index b5699ade4dabf..592f3266e14a5 100644 --- a/src/vscode-dts/vscode.proposed.nativeWindowHandle.d.ts +++ b/src/vscode-dts/vscode.proposed.nativeWindowHandle.d.ts @@ -9,9 +9,9 @@ declare module 'vscode' { export namespace env { /** - * Retrieves a base64 representation of a native window - * handle of the current window. + * Retrieves the native window handle of the current active window. + * The current active window may not be associated with this extension host. */ - export const handle: string | undefined; + export const nativeHandle: Uint8Array | undefined; } } From 521c6bcf9efff85b5e62fa1a2bc49249e0145b1c Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Thu, 21 Nov 2024 11:08:49 -0800 Subject: [PATCH 044/119] remote: broaden URI identification on openExternal (#234380) I want to protocol-activate copilot in a remote workspace using openExternal, but I observed the URI was being treated as a path and malformed. This change broadens the URI identification logic to accept anything that looks like a protocol. I think this is okay since we were already excluding file URIs from the rebase, so this really just impacts any other estoeric URIs in addition to the protocol `urlProtocol`. fyi @alexdima --- src/vs/server/node/server.cli.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/server/node/server.cli.ts b/src/vs/server/node/server.cli.ts index 4c821c003d5cf..eb68e8aa8b53f 100644 --- a/src/vs/server/node/server.cli.ts +++ b/src/vs/server/node/server.cli.ts @@ -375,7 +375,7 @@ function openInBrowser(args: string[], verbose: boolean) { const uris: string[] = []; for (const location of args) { try { - if (/^(http|https|file):\/\//.test(location)) { + if (/^[a-z-]+:\/\/.+/.test(location)) { uris.push(url.parse(location).href); } else { uris.push(pathToURI(location).href); From fb53d6aec9003a4dcffa96369a5e48bf821b77a5 Mon Sep 17 00:00:00 2001 From: Joyce Er Date: Thu, 21 Nov 2024 11:13:25 -0800 Subject: [PATCH 045/119] fix: serialize/deserialize working set URIs for restored chat requests (#234375) * fix: serialize/deserialize working set URIs for restored chat requests * tests: update snapshots --- src/vs/workbench/contrib/chat/common/chatModel.ts | 4 +++- .../common/__snapshots__/ChatService_can_deserialize.0.snap | 1 + .../ChatService_can_deserialize_with_response.0.snap | 1 + .../common/__snapshots__/ChatService_can_serialize.1.snap | 2 ++ .../common/__snapshots__/ChatService_sendRequest_fails.0.snap | 1 + 5 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/common/chatModel.ts b/src/vs/workbench/contrib/chat/common/chatModel.ts index c70bdf6ab0454..18e8eead70aa7 100644 --- a/src/vs/workbench/contrib/chat/common/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatModel.ts @@ -663,6 +663,7 @@ export interface ISerializableChatRequestData { isHidden: boolean; responseId?: string; agent?: ISerializableChatAgentData; + workingSet?: UriComponents[]; slashCommand?: IChatAgentCommand; // responseErrorDetails: IChatResponseErrorDetails | undefined; result?: IChatAgentResult; // Optional for backcompat @@ -1004,7 +1005,7 @@ export class ChatModel extends Disposable implements IChatModel { // Old messages don't have variableData, or have it in the wrong (non-array) shape const variableData: IChatRequestVariableData = this.reviveVariableData(raw.variableData); - const request = new ChatRequestModel(this, parsedRequest, variableData, raw.timestamp ?? -1, undefined, undefined, undefined, undefined, undefined, undefined, raw.requestId); + const request = new ChatRequestModel(this, parsedRequest, variableData, raw.timestamp ?? -1, undefined, undefined, undefined, undefined, raw.workingSet?.map((uri) => URI.revive(uri)), undefined, raw.requestId); request.isHidden = !!raw.isHidden; if (raw.response || raw.result || (raw as any).responseErrorDetails) { const agent = (raw.agent && 'metadata' in raw.agent) ? // Check for the new format, ignore entries in the old format @@ -1292,6 +1293,7 @@ export class ChatModel extends Disposable implements IChatModel { vote: r.response?.vote, voteDownReason: r.response?.voteDownReason, agent: agentJson, + workingSet: r.workingSet, slashCommand: r.response?.slashCommand, usedContext: r.response?.usedContext, contentReferences: r.response?.contentReferences, diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_deserialize.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_deserialize.0.snap index d6eda2dc105d2..c6069fc01267d 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_deserialize.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_deserialize.0.snap @@ -78,6 +78,7 @@ slashCommands: [ ], disambiguation: [ ] }, + workingSet: undefined, slashCommand: undefined, usedContext: { documents: [ diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_deserialize_with_response.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_deserialize_with_response.0.snap index f42a981fc872c..9bd998cda8894 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_deserialize_with_response.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_deserialize_with_response.0.snap @@ -78,6 +78,7 @@ slashCommands: [ ], disambiguation: [ ] }, + workingSet: undefined, slashCommand: undefined, usedContext: undefined, contentReferences: [ ], diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_serialize.1.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_serialize.1.snap index 8ad16f37294b7..39f80194a0b82 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_serialize.1.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_serialize.1.snap @@ -85,6 +85,7 @@ slashCommands: [ ], disambiguation: [ ] }, + workingSet: undefined, slashCommand: undefined, usedContext: { documents: [ @@ -153,6 +154,7 @@ disambiguation: [ ], isDefault: true }, + workingSet: undefined, slashCommand: undefined, usedContext: undefined, contentReferences: [ ], diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_sendRequest_fails.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_sendRequest_fails.0.snap index a4eeca5350e2d..5b280dc8bc6fe 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_sendRequest_fails.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_sendRequest_fails.0.snap @@ -78,6 +78,7 @@ slashCommands: [ ], disambiguation: [ ] }, + workingSet: undefined, slashCommand: undefined, usedContext: undefined, contentReferences: [ ], From cc236db03befe3a8304f02fb8d91cc4daa7426d9 Mon Sep 17 00:00:00 2001 From: Joyce Er Date: Thu, 21 Nov 2024 12:38:12 -0800 Subject: [PATCH 046/119] feat: allow removing files from working set (#234385) --- .../browser/chatEditing/chatEditingActions.ts | 44 ++++++++++++++++++- .../browser/chatEditing/chatEditingSession.ts | 20 ++++++--- .../contrib/chat/browser/chatInputPart.ts | 4 +- 3 files changed, 58 insertions(+), 10 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts index 45a79d8eaafc3..c52b0ae8f7cad 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts @@ -86,8 +86,8 @@ registerAction2(class RemoveFileFromWorkingSet extends WorkingSetAction { icon: Codicon.close, menu: [{ id: MenuId.ChatEditingWidgetModifiedFilesToolbar, - when: ContextKeyExpr.or(ContextKeyExpr.equals(chatEditingWidgetFileStateContextKey.key, WorkingSetEntryState.Attached), ContextKeyExpr.equals(chatEditingWidgetFileStateContextKey.key, WorkingSetEntryState.Suggested), ContextKeyExpr.equals(chatEditingWidgetFileStateContextKey.key, WorkingSetEntryState.Transient)), - order: 0, + // when: ContextKeyExpr.or(ContextKeyExpr.equals(chatEditingWidgetFileStateContextKey.key, WorkingSetEntryState.Attached), ContextKeyExpr.equals(chatEditingWidgetFileStateContextKey.key, WorkingSetEntryState.Suggested), ContextKeyExpr.equals(chatEditingWidgetFileStateContextKey.key, WorkingSetEntryState.Transient)), + order: 5, group: 'navigation' }], }); @@ -287,6 +287,46 @@ export class ChatEditingDiscardAllAction extends Action2 { } registerAction2(ChatEditingDiscardAllAction); +export class ChatEditingRemoveAllFilesAction extends Action2 { + static readonly ID = 'chatEditing.removeAllFiles'; + + constructor() { + super({ + id: ChatEditingRemoveAllFilesAction.ID, + title: localize('removeAll', 'Remove All'), + icon: Codicon.clearAll, + tooltip: localize('removeAllFiles', 'Remove All Files'), + precondition: ContextKeyExpr.and(ChatContextKeys.requestInProgress.negate()), + menu: [ + { + id: MenuId.ChatEditingWidgetToolbar, + group: 'navigation', + order: 5, + when: ContextKeyExpr.and(ChatContextKeys.location.isEqualTo(ChatAgentLocation.EditingSession)) + } + ] + }); + } + + async run(accessor: ServicesAccessor, ...args: any[]): Promise { + const chatEditingService = accessor.get(IChatEditingService); + const currentEditingSession = chatEditingService.currentEditingSession; + if (!currentEditingSession) { + return; + } + + const chatWidget = accessor.get(IChatWidgetService).getWidgetBySessionId(currentEditingSession.chatSessionId); + const uris = [...currentEditingSession.workingSet.keys()]; + currentEditingSession.remove(WorkingSetEntryRemovalReason.User, ...uris); + for (const uri of chatWidget?.attachmentModel.attachments ?? []) { + if (uri.isFile && URI.isUri(uri.value)) { + chatWidget?.attachmentModel.delete(uri.value.toString()); + } + } + } +} +registerAction2(ChatEditingRemoveAllFilesAction); + export class ChatEditingShowChangesAction extends Action2 { static readonly ID = 'chatEditing.viewChanges'; static readonly LABEL = localize('chatEditing.viewChanges', 'View All Edits'); diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts index 220fc5a30bfd1..d82246020c116 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts @@ -319,13 +319,21 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio let didRemoveUris = false; for (const uri of uris) { - const state = this._workingSet.get(uri); - if (state === undefined) { - continue; + + const entry = this._entriesObs.get().find(e => isEqual(e.modifiedURI, uri)); + if (entry) { + entry.dispose(); + const newEntries = this._entriesObs.get().filter(e => !isEqual(e.modifiedURI, uri)); + this._entriesObs.set(newEntries, undefined); + didRemoveUris = true; } - didRemoveUris = this._workingSet.delete(uri) || didRemoveUris; - if (reason === WorkingSetEntryRemovalReason.User && (state.state === WorkingSetEntryState.Transient || state.state === WorkingSetEntryState.Suggested)) { - this._removedTransientEntries.add(uri); + + const state = this._workingSet.get(uri); + if (state !== undefined) { + didRemoveUris = this._workingSet.delete(uri) || didRemoveUris; + if (reason === WorkingSetEntryRemovalReason.User && (state.state === WorkingSetEntryState.Transient || state.state === WorkingSetEntryState.Suggested)) { + this._removedTransientEntries.add(uri); + } } } diff --git a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts index 4158cc41cfe58..135718089b2bf 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts @@ -91,7 +91,7 @@ import { ChatAttachmentModel, EditsAttachmentModel } from './chatAttachmentModel import { IDisposableReference } from './chatContentParts/chatCollections.js'; import { CollapsibleListPool, IChatCollapsibleListItem } from './chatContentParts/chatReferencesContentPart.js'; import { ChatDragAndDrop, EditsDragAndDrop } from './chatDragAndDrop.js'; -import { ChatEditingShowChangesAction } from './chatEditing/chatEditingActions.js'; +import { ChatEditingRemoveAllFilesAction, ChatEditingShowChangesAction } from './chatEditing/chatEditingActions.js'; import { ChatEditingSaveAllAction } from './chatEditorSaving.js'; import { ChatFollowups } from './chatFollowups.js'; import { IChatViewState } from './chatWidget.js'; @@ -1180,7 +1180,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge arg: { sessionId: chatEditingSession.chatSessionId }, }, buttonConfigProvider: (action) => { - if (action.id === ChatEditingShowChangesAction.ID || action.id === ChatEditingSaveAllAction.ID) { + if (action.id === ChatEditingShowChangesAction.ID || action.id === ChatEditingSaveAllAction.ID || action.id === ChatEditingRemoveAllFilesAction.ID) { return { showIcon: true, showLabel: false, isSecondary: true }; } return undefined; From c24fb323998885fb0b0885d25e75d9d12519b339 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 21 Nov 2024 21:49:31 +0100 Subject: [PATCH 047/119] chat - make hide action globally accessible (#234383) --- .../workbench/contrib/chat/browser/chatSetup.contribution.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts b/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts index df27d811fdd82..26f4abedbfa52 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts @@ -426,6 +426,11 @@ class ChatSetupHideAction extends Action2 { super({ id: ChatSetupHideAction.ID, title: ChatSetupHideAction.TITLE, + f1: true, + precondition: ContextKeyExpr.and( + ChatContextKeys.Setup.triggered, + ChatContextKeys.Setup.installed.negate() + ), menu: { id: MenuId.ChatCommandCenter, group: 'a_first', From 8d2fb59cf63a171dd9accdcd17a14ad0833fb214 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Thu, 21 Nov 2024 16:28:42 -0500 Subject: [PATCH 048/119] improve folder/file terminal completions (#234363) --- extensions/terminal-suggest/README.md | 2 +- .../terminal-suggest/src/completions/cd.ts | 29 +++ .../src/terminalSuggestMain.ts | 190 +++++++++++------- .../browser/terminalCompletionService.ts | 70 ++++--- .../common/terminalSuggestConfiguration.ts | 2 +- 5 files changed, 180 insertions(+), 113 deletions(-) create mode 100644 extensions/terminal-suggest/src/completions/cd.ts diff --git a/extensions/terminal-suggest/README.md b/extensions/terminal-suggest/README.md index d93e4620567b3..adaffc410ac40 100644 --- a/extensions/terminal-suggest/README.md +++ b/extensions/terminal-suggest/README.md @@ -4,4 +4,4 @@ ## Features -Provides terminal suggestions for zsh, bash, and fish. +Provides terminal suggestions for zsh, bash, fish, and pwsh. diff --git a/extensions/terminal-suggest/src/completions/cd.ts b/extensions/terminal-suggest/src/completions/cd.ts new file mode 100644 index 0000000000000..89fc633911b27 --- /dev/null +++ b/extensions/terminal-suggest/src/completions/cd.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. + *--------------------------------------------------------------------------------------------*/ + +const cdSpec: Fig.Spec = { + name: 'cd', + description: 'Change the shell working directory', + args: { + name: 'folder', + template: 'folders', + isVariadic: true, + + suggestions: [ + { + name: '-', + description: 'Switch to the last used folder', + hidden: true, + }, + { + name: '~', + description: 'Switch to the home directory', + hidden: true, + }, + ], + } +}; + +export default cdSpec; diff --git a/extensions/terminal-suggest/src/terminalSuggestMain.ts b/extensions/terminal-suggest/src/terminalSuggestMain.ts index a8d537334e703..1ff88ed9eb6bf 100644 --- a/extensions/terminal-suggest/src/terminalSuggestMain.ts +++ b/extensions/terminal-suggest/src/terminalSuggestMain.ts @@ -9,6 +9,7 @@ import * as path from 'path'; import { ExecOptionsWithStringEncoding, execSync } from 'child_process'; import codeInsidersCompletionSpec from './completions/code-insiders'; import codeCompletionSpec from './completions/code'; +import cdSpec from './completions/cd'; let cachedAvailableCommands: Set | undefined; let cachedBuiltinCommands: Map | undefined; @@ -20,8 +21,7 @@ function getBuiltinCommands(shell: string): string[] | undefined { if (cachedCommands) { return cachedCommands; } - // fixes a bug with file/folder completions brought about by the '.' command - const filter = (cmd: string) => cmd && cmd !== '.'; + const filter = (cmd: string) => cmd; const options: ExecOptionsWithStringEncoding = { encoding: 'utf-8', shell }; switch (shellType) { case 'bash': { @@ -52,8 +52,11 @@ function getBuiltinCommands(shell: string): string[] | undefined { } break; } + case 'pwsh': { + // native pwsh completions are builtin to vscode + return []; + } } - // native pwsh completions are builtin to vscode return; } catch (error) { @@ -62,7 +65,6 @@ function getBuiltinCommands(shell: string): string[] | undefined { } } - export async function activate(context: vscode.ExtensionContext) { context.subscriptions.push(vscode.window.registerTerminalCompletionProvider({ id: 'terminal-suggest', @@ -87,12 +89,12 @@ export async function activate(context: vscode.ExtensionContext) { const items: vscode.TerminalCompletionItem[] = []; const prefix = getPrefix(terminalContext.commandLine, terminalContext.cursorPosition); - const specs = [codeCompletionSpec, codeInsidersCompletionSpec]; + const specs = [codeCompletionSpec, codeInsidersCompletionSpec, cdSpec]; const specCompletions = await getCompletionItemsFromSpecs(specs, terminalContext, new Set(commands), prefix, token); + items.push(...specCompletions.items); let filesRequested = specCompletions.filesRequested; let foldersRequested = specCompletions.foldersRequested; - items.push(...specCompletions.items); if (!specCompletions.specificSuggestionsProvided) { for (const command of commands) { @@ -106,26 +108,26 @@ export async function activate(context: vscode.ExtensionContext) { return undefined; } - const uniqueResults = new Map(); - for (const item of items) { - if (!uniqueResults.has(item.label)) { - uniqueResults.set(item.label, item); - } - } - const resultItems = uniqueResults.size ? Array.from(uniqueResults.values()) : undefined; + const shouldShowResourceCompletions = + ( + // If the command line is empty + terminalContext.commandLine.trim().length === 0 + // or no completions are found + || !items?.length + // or the completion found is '.' + || items.length === 1 && items[0].label === '.' + ) + // and neither files nor folders are going to be requested (for a specific spec's argument) + && (!filesRequested && !foldersRequested); - // If no completions are found, the prefix is a path, and neither files nor folders - // are going to be requested (for a specific spec's argument), show file/folder completions - const shouldShowResourceCompletions = !resultItems?.length && prefix.match(/^[./\\ ]/) && !filesRequested && !foldersRequested; if (shouldShowResourceCompletions) { filesRequested = true; foldersRequested = true; } - if (filesRequested || foldersRequested) { - return new vscode.TerminalCompletionList(resultItems, { filesRequested, foldersRequested, cwd: terminal.shellIntegration?.cwd, pathSeparator: shellPath.includes('/') ? '/' : '\\' }); + return new vscode.TerminalCompletionList(items, { filesRequested, foldersRequested, cwd: terminal.shellIntegration?.cwd, pathSeparator: osIsWindows() ? '\\' : '/' }); } - return resultItems; + return items; } })); } @@ -157,7 +159,7 @@ async function getCommandsInPath(): Promise | undefined> { if (cachedAvailableCommands) { return cachedAvailableCommands; } - const paths = os.platform() === 'win32' ? process.env.PATH?.split(';') : process.env.PATH?.split(':'); + const paths = osIsWindows() ? process.env.PATH?.split(';') : process.env.PATH?.split(':'); if (!paths) { return; } @@ -213,7 +215,7 @@ export function asArray(x: T | T[]): T[] { } function getCompletionItemsFromSpecs(specs: Fig.Spec[], terminalContext: { commandLine: string; cursorPosition: number }, availableCommands: Set, prefix: string, token: vscode.CancellationToken): { items: vscode.TerminalCompletionItem[]; filesRequested: boolean; foldersRequested: boolean; specificSuggestionsProvided: boolean } { - let items: vscode.TerminalCompletionItem[] = []; + const items: vscode.TerminalCompletionItem[] = []; let filesRequested = false; let foldersRequested = false; for (const spec of specs) { @@ -222,73 +224,105 @@ function getCompletionItemsFromSpecs(specs: Fig.Spec[], terminalContext: { comma continue; } for (const specLabel of specLabels) { - if (!availableCommands.has(specLabel) || token.isCancellationRequested) { + if (!availableCommands.has(specLabel) || token.isCancellationRequested || !terminalContext.commandLine.startsWith(specLabel)) { continue; } - if (terminalContext.commandLine.startsWith(specLabel)) { - if ('options' in spec && spec.options) { - for (const option of spec.options) { - const optionLabels = getLabel(option); - if (!optionLabels) { + const precedingText = terminalContext.commandLine.slice(0, terminalContext.cursorPosition + 1); + if ('options' in spec && spec.options) { + for (const option of spec.options) { + const optionLabels = getLabel(option); + if (!optionLabels) { + continue; + } + for (const optionLabel of optionLabels) { + if (optionLabel.startsWith(prefix) || (prefix.length > specLabel.length && prefix.trim() === specLabel)) { + items.push(createCompletionItem(terminalContext.cursorPosition, prefix, optionLabel, option.description, false, vscode.TerminalCompletionItemKind.Flag)); + } + const expectedText = `${specLabel} ${optionLabel} `; + if (!precedingText.includes(expectedText)) { continue; } - for (const optionLabel of optionLabels) { - if (optionLabel.startsWith(prefix) || (prefix.length > specLabel.length && prefix.trim() === specLabel)) { - items.push(createCompletionItem(terminalContext.cursorPosition, prefix, optionLabel, option.description, false, vscode.TerminalCompletionItemKind.Flag)); - } - if (!option.args) { - continue; - } - const args = asArray(option.args); - for (const arg of args) { - if (!arg) { - continue; - } - const precedingText = terminalContext.commandLine.slice(0, terminalContext.cursorPosition + 1); - const expectedText = `${specLabel} ${optionLabel} `; - if (!precedingText.includes(expectedText)) { - continue; - } - if (arg.template) { - if (arg.template === 'filepaths') { - if (precedingText.includes(expectedText)) { - filesRequested = true; - } - } else if (arg.template === 'folders') { - if (precedingText.includes(expectedText)) { - foldersRequested = true; - } - } - } - if (arg.suggestions?.length) { - // there are specific suggestions to show - items = []; - const indexOfPrecedingText = terminalContext.commandLine.lastIndexOf(expectedText); - const currentPrefix = precedingText.slice(indexOfPrecedingText + expectedText.length); - for (const suggestion of arg.suggestions) { - const suggestionLabels = getLabel(suggestion); - if (!suggestionLabels) { - continue; - } - for (const suggestionLabel of suggestionLabels) { - if (suggestionLabel && suggestionLabel.startsWith(currentPrefix.trim())) { - const hasSpaceBeforeCursor = terminalContext.commandLine[terminalContext.cursorPosition - 1] === ' '; - // prefix will be '' if there is a space before the cursor - items.push(createCompletionItem(terminalContext.cursorPosition, precedingText, suggestionLabel, arg.name, hasSpaceBeforeCursor, vscode.TerminalCompletionItemKind.Argument)); - } - } - } - if (items.length) { - return { items, filesRequested, foldersRequested, specificSuggestionsProvided: true }; - } - } - } + const indexOfPrecedingText = terminalContext.commandLine.lastIndexOf(expectedText); + const currentPrefix = precedingText.slice(indexOfPrecedingText + expectedText.length); + const argsCompletions = getCompletionItemsFromArgs(option.args, currentPrefix, terminalContext, precedingText); + if (!argsCompletions) { + continue; + } + if (argsCompletions.specificSuggestionsProvided) { + // prevents the list from containing a bunch of other stuff + return argsCompletions; } + items.push(...argsCompletions.items); + filesRequested = filesRequested || argsCompletions.filesRequested; + foldersRequested = foldersRequested || argsCompletions.foldersRequested; } } } + if ('args' in spec && asArray(spec.args)) { + const expectedText = `${specLabel} `; + if (!precedingText.includes(expectedText)) { + continue; + } + const indexOfPrecedingText = terminalContext.commandLine.lastIndexOf(expectedText); + const currentPrefix = precedingText.slice(indexOfPrecedingText + expectedText.length); + const argsCompletions = getCompletionItemsFromArgs(spec.args, currentPrefix, terminalContext, precedingText); + if (!argsCompletions) { + continue; + } + items.push(...argsCompletions.items); + filesRequested = filesRequested || argsCompletions.filesRequested; + foldersRequested = foldersRequested || argsCompletions.foldersRequested; + } } } return { items, filesRequested, foldersRequested, specificSuggestionsProvided: false }; } +function getCompletionItemsFromArgs(args: Fig.SingleOrArray | undefined, currentPrefix: string, terminalContext: { commandLine: string; cursorPosition: number }, precedingText: string): { items: vscode.TerminalCompletionItem[]; filesRequested: boolean; foldersRequested: boolean; specificSuggestionsProvided: boolean } | undefined { + if (!args) { + return; + } + + let items: vscode.TerminalCompletionItem[] = []; + let filesRequested = false; + let foldersRequested = false; + for (const arg of asArray(args)) { + if (!arg) { + continue; + } + if (arg.template) { + if (arg.template === 'filepaths') { + filesRequested = true; + } else if (arg.template === 'folders') { + foldersRequested = true; + } + } + if (arg.suggestions?.length) { + // there are specific suggestions to show + items = []; + for (const suggestion of arg.suggestions) { + const suggestionLabels = getLabel(suggestion); + if (!suggestionLabels) { + continue; + } + + for (const suggestionLabel of suggestionLabels) { + if (suggestionLabel && suggestionLabel.startsWith(currentPrefix.trim())) { + const hasSpaceBeforeCursor = terminalContext.commandLine[terminalContext.cursorPosition - 1] === ' '; + // prefix will be '' if there is a space before the cursor + const description = typeof suggestion !== 'string' ? suggestion.description : ''; + items.push(createCompletionItem(terminalContext.cursorPosition, precedingText, suggestionLabel, description, hasSpaceBeforeCursor, vscode.TerminalCompletionItemKind.Argument)); + } + } + } + if (items.length) { + return { items, filesRequested, foldersRequested, specificSuggestionsProvided: true }; + } + } + } + return { items, filesRequested, foldersRequested, specificSuggestionsProvided: false }; +} + +function osIsWindows(): boolean { + return os.platform() === 'win32'; +} diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts index 4e8379c847f93..bee024edb19eb 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { CancellationToken } from '../../../../../base/common/cancellation.js'; import { Disposable, IDisposable, toDisposable } from '../../../../../base/common/lifecycle.js'; +import { Schemas } from '../../../../../base/common/network.js'; import { URI } from '../../../../../base/common/uri.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; import { IFileService } from '../../../../../platform/files/common/files.js'; @@ -197,44 +198,47 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo } const resourceCompletions: ITerminalCompletion[] = []; - const fileStat = await this._fileService.resolve(cwd, { resolveSingleChildDescendants: true }); - if (!fileStat || !fileStat?.children) { - return; - } + const parentDirPath = cwd.fsPath.split(resourceRequestConfig.pathSeparator).slice(0, -1).join(resourceRequestConfig.pathSeparator); + const parentCwd = URI.from({ scheme: cwd.scheme, path: parentDirPath }); + const dirToPrefixMap = new Map(); - for (const stat of fileStat.children) { - let kind: TerminalCompletionItemKind | undefined; - if (foldersRequested && stat.isDirectory) { - kind = TerminalCompletionItemKind.Folder; - } - if (filesRequested && !stat.isDirectory && (stat.isFile || stat.resource.scheme === 'file')) { - kind = TerminalCompletionItemKind.File; - } - if (kind === undefined) { - continue; - } - const lastWord = promptValue.substring(0, cursorPosition).split(' ').pop(); - const lastIndexOfDot = lastWord?.lastIndexOf('.') ?? -1; - const lastIndexOfSlash = lastWord?.lastIndexOf(resourceRequestConfig.pathSeparator) ?? -1; - let label; - if (lastIndexOfSlash > -1) { - label = stat.resource.fsPath.replace(cwd.fsPath, '').substring(1); - } else if (lastIndexOfDot === -1) { - label = '.' + stat.resource.fsPath.replace(cwd.fsPath, ''); - } else { - label = stat.resource.fsPath.replace(cwd.fsPath, ''); + dirToPrefixMap.set(cwd, '.'); + dirToPrefixMap.set(parentCwd, '..'); + + const lastWord = promptValue.substring(0, cursorPosition).split(' ').pop() ?? ''; + + for (const [dir, prefix] of dirToPrefixMap) { + const fileStat = await this._fileService.resolve(dir, { resolveSingleChildDescendants: true }); + + if (!fileStat || !fileStat?.children) { + return; } - resourceCompletions.push({ - label, - kind, - isDirectory: kind === TerminalCompletionItemKind.Folder, - isFile: kind === TerminalCompletionItemKind.File, - replacementIndex: cursorPosition, - replacementLength: label.length - }); + for (const stat of fileStat.children) { + let kind: TerminalCompletionItemKind | undefined; + if (foldersRequested && stat.isDirectory) { + kind = TerminalCompletionItemKind.Folder; + } + if (filesRequested && !stat.isDirectory && (stat.isFile || stat.resource.scheme === Schemas.file)) { + kind = TerminalCompletionItemKind.File; + } + if (kind === undefined) { + continue; + } + + const label = prefix + stat.resource.fsPath.replace(cwd.fsPath, ''); + resourceCompletions.push({ + label, + kind, + isDirectory: kind === TerminalCompletionItemKind.Folder, + isFile: kind === TerminalCompletionItemKind.File, + replacementIndex: cursorPosition - lastWord.length, + replacementLength: label.length + }); + } } + return resourceCompletions.length ? resourceCompletions : undefined; } } diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/common/terminalSuggestConfiguration.ts b/src/vs/workbench/contrib/terminalContrib/suggest/common/terminalSuggestConfiguration.ts index 0779fb33879dd..860bb28015acc 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/common/terminalSuggestConfiguration.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/common/terminalSuggestConfiguration.ts @@ -34,7 +34,7 @@ export interface ITerminalSuggestConfiguration { export const terminalSuggestConfiguration: IStringDictionary = { [TerminalSuggestSettingId.Enabled]: { restricted: true, - markdownDescription: localize('suggest.enabled', "Enables experimental terminal Intellisense suggestions for supported shells ({0}) when {1} is set to {2}.\n\nIf shell integration is installed manually, {3} needs to be set to {4} before calling the shell integration script. \n\nFor zsh and bash completions, {5} will also need to be set.", 'PowerShell v7+, zsh, bash', `\`#${TerminalSettingId.ShellIntegrationEnabled}#\``, '`true`', '`VSCODE_SUGGEST`', '`1`', `\`#${TerminalSuggestSettingId.EnableExtensionCompletions}#\``), + markdownDescription: localize('suggest.enabled', "Enables experimental terminal Intellisense suggestions for supported shells ({0}) when {1} is set to {2}.\n\nIf shell integration is installed manually, {3} needs to be set to {4} before calling the shell integration script. \n\nFor extension provided completions, {5} will also need to be set.", 'PowerShell v7+, zsh, bash, fish', `\`#${TerminalSettingId.ShellIntegrationEnabled}#\``, '`true`', '`VSCODE_SUGGEST`', '`1`', `\`#${TerminalSuggestSettingId.EnableExtensionCompletions}#\``), type: 'boolean', default: false, tags: ['experimental'], From 2d9cde334d1b7abce1ed2fe3319ade11231f96de Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Thu, 21 Nov 2024 23:18:49 +0100 Subject: [PATCH 049/119] Git - tweak git blame computation (#234386) * Helper methods * Finished implementing the prototype * Command handled model creation/disposal * Cache staged resources diff information --- extensions/git/src/blame.ts | 149 +++++++++--------- extensions/git/src/git.ts | 11 +- extensions/git/src/repository.ts | 4 +- .../browser/parts/editor/editorCommands.ts | 31 ++++ 4 files changed, 120 insertions(+), 75 deletions(-) diff --git a/extensions/git/src/blame.ts b/extensions/git/src/blame.ts index 10c8e0d98f21e..fc0f74fcf169a 100644 --- a/extensions/git/src/blame.ts +++ b/extensions/git/src/blame.ts @@ -3,15 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { DecorationOptions, l10n, Position, Range, TextEditor, TextEditorChange, TextEditorDecorationType, TextEditorChangeKind, ThemeColor, Uri, window, workspace, EventEmitter, ConfigurationChangeEvent, StatusBarItem, StatusBarAlignment, Command, MarkdownString } from 'vscode'; +import { DecorationOptions, l10n, Position, Range, TextEditor, TextEditorChange, TextEditorDecorationType, TextEditorChangeKind, ThemeColor, Uri, window, workspace, EventEmitter, ConfigurationChangeEvent, StatusBarItem, StatusBarAlignment, Command, MarkdownString, commands, LineChange } from 'vscode'; import { Model } from './model'; import { dispose, fromNow, IDisposable, pathEquals } from './util'; import { Repository } from './repository'; import { throttle } from './decorators'; import { BlameInformation } from './git'; -const notCommittedYetId = '0000000000000000000000000000000000000000'; - function isLineChanged(lineNumber: number, changes: readonly TextEditorChange[]): boolean { for (const change of changes) { // If the change is a delete, skip it @@ -64,28 +62,6 @@ function mapLineNumber(lineNumber: number, changes: readonly TextEditorChange[]) return lineNumber; } -function processTextEditorChangesWithBlameInformation(blameInformation: BlameInformation[], changes: readonly TextEditorChange[]): TextEditorChange[] { - const [notYetCommittedBlameInformation] = blameInformation.filter(b => b.id === notCommittedYetId); - if (!notYetCommittedBlameInformation) { - return [...changes]; - } - - const changesWithBlameInformation: TextEditorChange[] = []; - for (const change of changes) { - const originalStartLineNumber = mapLineNumber(change.originalStartLineNumber, changes); - const originalEndLineNumber = mapLineNumber(change.originalEndLineNumber, changes); - - if (notYetCommittedBlameInformation.ranges.some(range => - range.startLineNumber === originalStartLineNumber && range.endLineNumber === originalEndLineNumber)) { - continue; - } - - changesWithBlameInformation.push(change); - } - - return changesWithBlameInformation; -} - function getBlameInformationHover(documentUri: Uri, blameInformation: BlameInformation | string): MarkdownString { if (typeof blameInformation === 'string') { return new MarkdownString(blameInformation, true); @@ -123,12 +99,7 @@ function getBlameInformationHover(documentUri: Uri, blameInformation: BlameInfor interface RepositoryBlameInformation { readonly commit: string; /* commit used for blame information */ - readonly blameInformation: Map; -} - -interface ResourceBlameInformation { - readonly staged: boolean; /* whether the file is staged */ - readonly blameInformation: BlameInformation[]; + readonly blameInformation: Map; } interface LineBlameInformation { @@ -141,7 +112,9 @@ export class GitBlameController { public readonly onDidChangeBlameInformation = this._onDidChangeBlameInformation.event; readonly textEditorBlameInformation = new Map(); + private readonly _repositoryBlameInformation = new Map(); + private readonly _stagedResourceDiffInformation = new Map>(); private _repositoryDisposables = new Map(); private _disposables: IDisposable[] = []; @@ -163,6 +136,8 @@ export class GitBlameController { const repositoryDisposables: IDisposable[] = []; repository.onDidRunGitStatus(() => this._onDidRunGitStatus(repository), this, repositoryDisposables); + repository.onDidChangeRepository(e => this._onDidChangeRepository(repository, e), this, this._disposables); + this._repositoryDisposables.set(repository, repositoryDisposables); } @@ -177,38 +152,29 @@ export class GitBlameController { } private _onDidRunGitStatus(repository: Repository): void { - let repositoryBlameInformation = this._repositoryBlameInformation.get(repository); + const repositoryBlameInformation = this._repositoryBlameInformation.get(repository); if (!repositoryBlameInformation) { return; } - let updateDecorations = false; - - // 1. HEAD commit changed (remove all blame information for the repository) + // HEAD commit changed (remove blame information for the repository) if (repositoryBlameInformation.commit !== repository.HEAD?.commit) { this._repositoryBlameInformation.delete(repository); - repositoryBlameInformation = undefined; - updateDecorations = true; - } - - // 2. Resource has been staged/unstaged (remove blame information for the resource) - for (const [uri, resourceBlameInformation] of repositoryBlameInformation?.blameInformation.entries() ?? []) { - const isStaged = repository.indexGroup.resourceStates - .some(r => pathEquals(uri.fsPath, r.resourceUri.fsPath)); - if (resourceBlameInformation.staged !== isStaged) { - repositoryBlameInformation?.blameInformation.delete(uri); - updateDecorations = true; - } - } - - if (updateDecorations) { for (const textEditor of window.visibleTextEditors) { this._updateTextEditorBlameInformation(textEditor); } } } + private _onDidChangeRepository(repository: Repository, uri: Uri): void { + if (!/\.git\/index$/.test(uri.fsPath)) { + return; + } + + this._stagedResourceDiffInformation.delete(repository); + } + private async _getBlameInformation(resource: Uri): Promise { const repository = this._model.getRepository(resource); if (!repository || !repository.HEAD?.commit) { @@ -217,25 +183,66 @@ export class GitBlameController { const repositoryBlameInformation = this._repositoryBlameInformation.get(repository) ?? { commit: repository.HEAD.commit, - blameInformation: new Map() + blameInformation: new Map() } satisfies RepositoryBlameInformation; let resourceBlameInformation = repositoryBlameInformation.blameInformation.get(resource); if (repositoryBlameInformation.commit === repository.HEAD.commit && resourceBlameInformation) { - return resourceBlameInformation.blameInformation; + return resourceBlameInformation; } - const staged = repository.indexGroup.resourceStates - .some(r => pathEquals(resource.fsPath, r.resourceUri.fsPath)); - const blameInformation = await repository.blame2(resource.fsPath) ?? []; - resourceBlameInformation = { staged, blameInformation } satisfies ResourceBlameInformation; + // Get blame information for the resource + resourceBlameInformation = await repository.blame2(resource.fsPath, repository.HEAD.commit) ?? []; this._repositoryBlameInformation.set(repository, { ...repositoryBlameInformation, blameInformation: repositoryBlameInformation.blameInformation.set(resource, resourceBlameInformation) }); - return resourceBlameInformation.blameInformation; + return resourceBlameInformation; + } + + private async _getStagedResourceDiffInformation(uri: Uri): Promise { + const repository = this._model.getRepository(uri); + if (!repository) { + return undefined; + } + + const [resource] = repository.indexGroup + .resourceStates.filter(r => pathEquals(uri.fsPath, r.resourceUri.fsPath)); + + if (!resource || !resource.leftUri || !resource.rightUri) { + return undefined; + } + + const diffInformationMap = this._stagedResourceDiffInformation.get(repository) ?? new Map(); + let changes = diffInformationMap.get(resource.resourceUri); + if (changes) { + return changes; + } + + // Get the diff information for the staged resource + const diffInformation: LineChange[] = await commands.executeCommand('_workbench.internal.computeDirtyDiff', resource.leftUri, resource.rightUri); + if (!diffInformation) { + return undefined; + } + + changes = diffInformation.map(change => { + const kind = change.originalEndLineNumber === 0 ? TextEditorChangeKind.Addition : + change.modifiedEndLineNumber === 0 ? TextEditorChangeKind.Deletion : TextEditorChangeKind.Modification; + + return { + originalStartLineNumber: change.originalStartLineNumber, + originalEndLineNumber: change.originalEndLineNumber, + modifiedStartLineNumber: change.modifiedStartLineNumber, + modifiedEndLineNumber: change.modifiedEndLineNumber, + kind + } satisfies TextEditorChange; + }); + + this._stagedResourceDiffInformation.set(repository, diffInformationMap.set(resource.resourceUri, changes)); + + return changes; } @throttle @@ -250,13 +257,9 @@ export class GitBlameController { return; } - // Remove the diff information that is contained in the git blame information. - // This is done since git blame information is the source of truth and we don't - // need the diff information for those ranges. The complete diff information is - // still used to determine whether a line is changed or not. - const diffInformationWithBlame = processTextEditorChangesWithBlameInformation( - resourceBlameInformation, - diffInformation.changes); + // The diff information does not contain changes that have been staged. We need + // to get the staged changes and if present, merge them with the diff information. + const diffInformationStagedResources: TextEditorChange[] = await this._getStagedResourceDiffInformation(textEditor.document.uri) ?? []; const lineBlameInformation: LineBlameInformation[] = []; for (const lineNumber of textEditor.selections.map(s => s.active.line)) { @@ -266,8 +269,16 @@ export class GitBlameController { continue; } - // Map the line number to the git blame ranges - const lineNumberWithDiff = mapLineNumber(lineNumber + 1, diffInformationWithBlame); + // Check if the line is contained in the staged resources diff information + if (isLineChanged(lineNumber + 1, diffInformationStagedResources)) { + lineBlameInformation.push({ lineNumber, blameInformation: l10n.t('Not Committed Yet (Staged)') }); + continue; + } + + const diffInformationAll = [...diffInformation.changes, ...diffInformationStagedResources]; + + // Map the line number to the git blame ranges using the diff information + const lineNumberWithDiff = mapLineNumber(lineNumber + 1, diffInformationAll); const blameInformation = resourceBlameInformation.find(blameInformation => { return blameInformation.ranges.find(range => { return lineNumberWithDiff >= range.startLineNumber && lineNumberWithDiff <= range.endLineNumber; @@ -275,11 +286,7 @@ export class GitBlameController { }); if (blameInformation) { - if (blameInformation.id !== notCommittedYetId) { - lineBlameInformation.push({ lineNumber, blameInformation }); - } else { - lineBlameInformation.push({ lineNumber, blameInformation: l10n.t('Not Committed Yet (Staged)') }); - } + lineBlameInformation.push({ lineNumber, blameInformation }); } } diff --git a/extensions/git/src/git.ts b/extensions/git/src/git.ts index e81093703c513..155a52128b0c8 100644 --- a/extensions/git/src/git.ts +++ b/extensions/git/src/git.ts @@ -2207,9 +2207,16 @@ export class Repository { } } - async blame2(path: string): Promise { + async blame2(path: string, ref?: string): Promise { try { - const args = ['blame', '--root', '--incremental', '--', sanitizePath(path)]; + const args = ['blame', '--root', '--incremental']; + + if (ref) { + args.push(ref); + } + + args.push('--', sanitizePath(path)); + const result = await this.exec(args); return parseGitBlame(result.stdout.trim()); diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index 167130266ccdc..89f3221b6fb4b 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -1786,8 +1786,8 @@ export class Repository implements Disposable { return await this.run(Operation.Blame(true), () => this.repository.blame(path)); } - async blame2(path: string): Promise { - return await this.run(Operation.Blame(false), () => this.repository.blame2(path)); + async blame2(path: string, ref?: string): Promise { + return await this.run(Operation.Blame(false), () => this.repository.blame2(path, ref)); } @throttle diff --git a/src/vs/workbench/browser/parts/editor/editorCommands.ts b/src/vs/workbench/browser/parts/editor/editorCommands.ts index 1cecdaab32f09..0ab2d529df23d 100644 --- a/src/vs/workbench/browser/parts/editor/editorCommands.ts +++ b/src/vs/workbench/browser/parts/editor/editorCommands.ts @@ -40,6 +40,8 @@ import { IPathService } from '../../../services/path/common/pathService.js'; import { IUntitledTextEditorService } from '../../../services/untitled/common/untitledTextEditorService.js'; import { DIFF_FOCUS_OTHER_SIDE, DIFF_FOCUS_PRIMARY_SIDE, DIFF_FOCUS_SECONDARY_SIDE, DIFF_OPEN_SIDE, registerDiffEditorCommands } from './diffEditorCommands.js'; import { IResolvedEditorCommandsContext, resolveCommandsContext } from './editorCommandsContext.js'; +import { IEditorWorkerService } from '../../../../editor/common/services/editorWorker.js'; +import { ITextModelService } from '../../../../editor/common/services/resolverService.js'; export const CLOSE_SAVED_EDITORS_COMMAND_ID = 'workbench.action.closeUnmodifiedEditors'; export const CLOSE_EDITORS_IN_GROUP_COMMAND_ID = 'workbench.action.closeEditorsInGroup'; @@ -536,6 +538,35 @@ function registerOpenEditorAPICommands(): void { label: options.title, }); }); + + CommandsRegistry.registerCommand('_workbench.internal.computeDirtyDiff', async (accessor: ServicesAccessor, original: UriComponents, modified: UriComponents) => { + const configurationService = accessor.get(IConfigurationService); + const editorWorkerService = accessor.get(IEditorWorkerService); + const textModelService = accessor.get(ITextModelService); + + const originalResource = URI.revive(original); + const modifiedResource = URI.revive(modified); + + const originalModel = await textModelService.createModelReference(originalResource); + const modifiedModel = await textModelService.createModelReference(modifiedResource); + + const canComputeDirtyDiff = editorWorkerService.canComputeDirtyDiff(originalResource, modifiedResource); + if (!canComputeDirtyDiff) { + return undefined; + } + + const ignoreTrimWhitespaceSetting = configurationService.getValue<'true' | 'false' | 'inherit'>('scm.diffDecorationsIgnoreTrimWhitespace'); + const ignoreTrimWhitespace = ignoreTrimWhitespaceSetting === 'inherit' + ? configurationService.getValue('diffEditor.ignoreTrimWhitespace') + : ignoreTrimWhitespaceSetting !== 'false'; + + const changes = await editorWorkerService.computeDirtyDiff(originalResource, modifiedResource, ignoreTrimWhitespace); + + originalModel.dispose(); + modifiedModel.dispose(); + + return changes; + }); } interface OpenMultiFileDiffEditorOptions { From 9f504fcd6e5b6c3dddf3293115d061f1f489f8a8 Mon Sep 17 00:00:00 2001 From: Michael Lively Date: Thu, 21 Nov 2024 14:43:47 -0800 Subject: [PATCH 050/119] Add markdown cell toolbar entry for run in section (#234387) * add markdown cell toolbar entry for run in section * remove unnecesasry context key and --- .../contrib/outline/notebookOutline.ts | 21 ++++--- .../browser/controller/coreActions.ts | 1 + .../browser/controller/executeActions.ts | 4 ++ .../browser/controller/sectionActions.ts | 60 +++++++++++++------ .../viewParts/notebookEditorStickyScroll.ts | 4 +- 5 files changed, 61 insertions(+), 29 deletions(-) diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline.ts b/src/vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline.ts index b0409762b8d90..7df78960c43d4 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline.ts @@ -44,7 +44,7 @@ import { Action2, IMenu, IMenuService, MenuId, MenuItemAction, MenuRegistry, reg import { ContextKeyExpr, IContextKeyService, RawContextKey } from '../../../../../../platform/contextkey/common/contextkey.js'; import { MenuEntryActionViewItem, getActionBarActions } from '../../../../../../platform/actions/browser/menuEntryActionViewItem.js'; import { IAction } from '../../../../../../base/common/actions.js'; -import { NotebookSectionArgs } from '../../controller/sectionActions.js'; +import { NotebookOutlineEntryArgs } from '../../controller/sectionActions.js'; import { MarkupCellViewModel } from '../../viewModel/markupCellViewModel.js'; import { Delayer, disposableTimeout } from '../../../../../../base/common/async.js'; import { IOutlinePane } from '../../../../outline/browser/outline.js'; @@ -153,8 +153,12 @@ class NotebookOutlineRenderer implements ITreeRenderer void) | undefined; @@ -218,13 +222,13 @@ class NotebookOutlineRenderer implements ITreeRenderer { if (dropdownIsVisible) { - const actions = getOutlineToolbarActions(menu, { notebookEditor: this._editor, outlineEntry: entry }); + const actions = getOutlineToolbarActions(menu, { notebookEditor: editor, outlineEntry: entry }); deferredUpdate = () => toolbar.setActions(actions.primary, actions.secondary); return; } - const actions = getOutlineToolbarActions(menu, { notebookEditor: this._editor, outlineEntry: entry }); + const actions = getOutlineToolbarActions(menu, { notebookEditor: editor, outlineEntry: entry }); toolbar.setActions(actions.primary, actions.secondary); })); @@ -249,9 +253,8 @@ class NotebookOutlineRenderer implements ITreeRenderer /^inline/.test(g)); +function getOutlineToolbarActions(menu: IMenu, args?: NotebookOutlineEntryArgs): { primary: IAction[]; secondary: IAction[] } { + return getActionBarActions(menu.getActions({ shouldForwardArgs: true, arg: args }), g => /^inline/.test(g)); } class NotebookOutlineAccessibility implements IListAccessibilityProvider { diff --git a/src/vs/workbench/contrib/notebook/browser/controller/coreActions.ts b/src/vs/workbench/contrib/notebook/browser/controller/coreActions.ts index 567ae7a8ede3c..6d986184bad3d 100644 --- a/src/vs/workbench/contrib/notebook/browser/controller/coreActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/controller/coreActions.ts @@ -34,6 +34,7 @@ export const NOTEBOOK_EDITOR_WIDGET_ACTION_WEIGHT = KeybindingWeight.EditorContr export const NOTEBOOK_OUTPUT_WEBVIEW_ACTION_WEIGHT = KeybindingWeight.WorkbenchContrib + 1; // higher than Workbench contribution (such as Notebook List View), etc export const enum CellToolbarOrder { + RunSection, EditCell, ExecuteAboveCells, ExecuteCellAndBelow, diff --git a/src/vs/workbench/contrib/notebook/browser/controller/executeActions.ts b/src/vs/workbench/contrib/notebook/browser/controller/executeActions.ts index bcb4fcc526adb..57fc0cfc08f95 100644 --- a/src/vs/workbench/contrib/notebook/browser/controller/executeActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/controller/executeActions.ts @@ -56,6 +56,10 @@ export const executeThisCellCondition = ContextKeyExpr.and( executeCondition, NOTEBOOK_CELL_EXECUTING.toNegated()); +export const executeSectionCondition = ContextKeyExpr.and( + NOTEBOOK_CELL_TYPE.isEqualTo('markup'), +); + function renderAllMarkdownCells(context: INotebookActionContext): void { for (let i = 0; i < context.notebookEditor.getLength(); i++) { const cell = context.notebookEditor.cellAt(i); diff --git a/src/vs/workbench/contrib/notebook/browser/controller/sectionActions.ts b/src/vs/workbench/contrib/notebook/browser/controller/sectionActions.ts index f51e058a09fae..91dff4b7014a6 100644 --- a/src/vs/workbench/contrib/notebook/browser/controller/sectionActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/controller/sectionActions.ts @@ -9,20 +9,22 @@ import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contex import { ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js'; import { NotebookOutlineContext } from '../contrib/outline/notebookOutline.js'; import { FoldingController } from './foldingController.js'; -import { CellFoldingState, INotebookEditor } from '../notebookBrowser.js'; +import { CellFoldingState, ICellViewModel, INotebookEditor } from '../notebookBrowser.js'; import * as icons from '../notebookIcons.js'; import { OutlineEntry } from '../viewModel/OutlineEntry.js'; import { CellKind } from '../../common/notebookCommon.js'; import { OutlineTarget } from '../../../../services/outline/browser/outline.js'; +import { CELL_TITLE_CELL_GROUP_ID, CellToolbarOrder } from './coreActions.js'; +import { executeSectionCondition } from './executeActions.js'; -export type NotebookSectionArgs = { - notebookEditor: INotebookEditor | undefined; +export type NotebookOutlineEntryArgs = { + notebookEditor: INotebookEditor; outlineEntry: OutlineEntry; }; -export type ValidNotebookSectionArgs = { +export type NotebookCellArgs = { notebookEditor: INotebookEditor; - outlineEntry: OutlineEntry; + cell: ICellViewModel; }; export class NotebookRunSingleCellInSection extends Action2 { @@ -51,8 +53,8 @@ export class NotebookRunSingleCellInSection extends Action2 { }); } - override async run(_accessor: ServicesAccessor, context: NotebookSectionArgs): Promise { - if (!checkSectionContext(context)) { + override async run(_accessor: ServicesAccessor, context: any): Promise { + if (!checkOutlineEntryContext(context)) { return; } @@ -69,7 +71,7 @@ export class NotebookRunCellsInSection extends Action2 { mnemonicTitle: localize({ key: 'mirunCellsInSection', comment: ['&& denotes a mnemonic'] }, "&&Run Cells In Section"), }, shortTitle: localize('runCellsInSection', "Run Cells In Section"), - // icon: icons.executeBelowIcon, // TODO @Yoyokrazy replace this with new icon later + icon: icons.executeIcon, // TODO @Yoyokrazy replace this with new icon later menu: [ { id: MenuId.NotebookStickyScrollContext, @@ -86,17 +88,27 @@ export class NotebookRunCellsInSection extends Action2 { NotebookOutlineContext.CellHasChildren, NotebookOutlineContext.CellHasHeader, ) + }, + { + id: MenuId.NotebookCellTitle, + order: CellToolbarOrder.RunSection, + group: CELL_TITLE_CELL_GROUP_ID, + when: executeSectionCondition } ] }); } - override async run(_accessor: ServicesAccessor, context: NotebookSectionArgs): Promise { - if (!checkSectionContext(context)) { + override async run(_accessor: ServicesAccessor, context: any): Promise { + let cell: ICellViewModel; + if (checkOutlineEntryContext(context)) { + cell = context.outlineEntry.cell; + } else if (checkNotebookCellContext(context)) { + cell = context.cell; + } else { return; } - const cell = context.outlineEntry.cell; const idx = context.notebookEditor.getViewModel()?.getCellIndex(cell); if (idx === undefined) { return; @@ -137,8 +149,8 @@ export class NotebookFoldSection extends Action2 { }); } - override async run(_accessor: ServicesAccessor, context: NotebookSectionArgs): Promise { - if (!checkSectionContext(context)) { + override async run(_accessor: ServicesAccessor, context: any): Promise { + if (!checkOutlineEntryContext(context)) { return; } @@ -181,8 +193,8 @@ export class NotebookExpandSection extends Action2 { }); } - override async run(_accessor: ServicesAccessor, context: NotebookSectionArgs): Promise { - if (!checkSectionContext(context)) { + override async run(_accessor: ServicesAccessor, context: any): Promise { + if (!checkOutlineEntryContext(context)) { return; } @@ -200,15 +212,27 @@ export class NotebookExpandSection extends Action2 { } /** - * Take in context args and check if they exist + * Take in context args and check if they exist. True if action is run from notebook sticky scroll context menu or + * notebook outline context menu. * - * @param context - Notebook Section Context containing a notebook editor and outline entry + * @param context - Notebook Outline Context containing a notebook editor and outline entry * @returns true if context is valid, false otherwise */ -function checkSectionContext(context: NotebookSectionArgs): context is ValidNotebookSectionArgs { +function checkOutlineEntryContext(context: any): context is NotebookOutlineEntryArgs { return !!(context && context.notebookEditor && context.outlineEntry); } +/** + * Take in context args and check if they exist. True if action is run from a cell toolbar menu (potentially from the + * notebook cell container or cell editor context menus, but not tested or implemented atm) + * + * @param context - Notebook Outline Context containing a notebook editor and outline entry + * @returns true if context is valid, false otherwise + */ +function checkNotebookCellContext(context: any): context is NotebookCellArgs { + return !!(context && context.notebookEditor && context.cell); +} + registerAction2(NotebookRunSingleCellInSection); registerAction2(NotebookRunCellsInSection); registerAction2(NotebookFoldSection); diff --git a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorStickyScroll.ts b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorStickyScroll.ts index f742c0f3e67a4..c2e6f78366e21 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorStickyScroll.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorStickyScroll.ts @@ -21,7 +21,7 @@ import { foldingCollapsedIcon, foldingExpandedIcon } from '../../../../../editor import { MarkupCellViewModel } from '../viewModel/markupCellViewModel.js'; import { FoldingController } from '../controller/foldingController.js'; import { NotebookOptionsChangeEvent } from '../notebookOptions.js'; -import { NotebookSectionArgs } from '../controller/sectionActions.js'; +import { NotebookOutlineEntryArgs } from '../controller/sectionActions.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { INotebookCellOutlineDataSourceFactory } from '../viewModel/notebookOutlineDataSourceFactory.js'; @@ -175,7 +175,7 @@ export class NotebookStickyScroll extends Disposable { return; } - const args: NotebookSectionArgs = { + const args: NotebookOutlineEntryArgs = { outlineEntry: selectedOutlineEntry, notebookEditor: this.notebookEditor, }; From 73a4532506d99e20a75c91e9834e276ff98f14eb Mon Sep 17 00:00:00 2001 From: Joyce Er Date: Thu, 21 Nov 2024 16:43:24 -0800 Subject: [PATCH 051/119] feat: render +/- diff summary in completed code block pill (#234392) --- .../chatContentParts/chatMarkdownContentPart.ts | 16 ++++++++++++++++ .../chat/browser/media/chatCodeBlockPill.css | 12 ++++++++++++ 2 files changed, 28 insertions(+) diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts index e07c684a330a5..4cd638dedf1a0 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts @@ -374,6 +374,22 @@ class CollapsedCodeBlock extends Disposable { iconEl.classList.add(...getIconClasses(this.modelService, this.languageService, uri, fileKind)); labelDetail.textContent = ''; } + + if (!isStreaming && isComplete) { + const labelAdded = this.element.querySelector('.label-added') ?? this.element.appendChild(dom.$('span.label-added')); + const labelRemoved = this.element.querySelector('.label-removed') ?? this.element.appendChild(dom.$('span.label-removed')); + const changes = modifiedEntry?.diffInfo.read(r); + if (changes && !changes?.identical && !changes?.quitEarly) { + let removedLines = 0; + let addedLines = 0; + for (const change of changes.changes) { + removedLines += change.original.endLineNumberExclusive - change.original.startLineNumber; + addedLines += change.modified.endLineNumberExclusive - change.modified.startLineNumber; + } + labelAdded.textContent = `+${addedLines}`; + labelRemoved.textContent = `-${removedLines}`; + } + } })); } } diff --git a/src/vs/workbench/contrib/chat/browser/media/chatCodeBlockPill.css b/src/vs/workbench/contrib/chat/browser/media/chatCodeBlockPill.css index 24462f9d2e0db..b7f341054b425 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chatCodeBlockPill.css +++ b/src/vs/workbench/contrib/chat/browser/media/chatCodeBlockPill.css @@ -50,3 +50,15 @@ span.label-detail { font-style: italic; color: var(--vscode-descriptionForeground); } + +span.label-added { + font-weight: bold; + padding-left: 4px; + color: var(--vscode-editorGutter-addedBackground); +} + +span.label-removed { + font-weight: bold; + padding-left: 4px; + color: var(--vscode-editorGutter-deletedBackground); +} From 4fbde8be3ae514e5923f86b08e6d824769089a98 Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt Date: Thu, 21 Nov 2024 16:51:00 -0800 Subject: [PATCH 052/119] Update the handle if the user uses aux windows (#234394) that way the broker always shows on top of the current focused window. --- src/vs/platform/native/common/native.ts | 1 + .../native/electron-main/nativeHostMainService.ts | 8 ++++++++ src/vs/workbench/api/browser/mainThreadWindow.ts | 13 +++++++++++++ src/vs/workbench/api/common/extHost.protocol.ts | 1 + src/vs/workbench/api/common/extHostWindow.ts | 4 ++++ .../services/host/browser/browserHostService.ts | 7 +++++++ src/vs/workbench/services/host/browser/host.ts | 10 ++++++++++ .../host/electron-sandbox/nativeHostService.ts | 13 +++++++++++++ .../workbench/test/browser/workbenchTestServices.ts | 2 ++ .../test/electron-sandbox/workbenchTestServices.ts | 1 + 10 files changed, 60 insertions(+) diff --git a/src/vs/platform/native/common/native.ts b/src/vs/platform/native/common/native.ts index b3333190d1b62..7ec630cb84430 100644 --- a/src/vs/platform/native/common/native.ts +++ b/src/vs/platform/native/common/native.ts @@ -75,6 +75,7 @@ export interface ICommonNativeHostService { getWindowCount(): Promise; getActiveWindowId(): Promise; getActiveWindowPosition(): Promise; + getNativeWindowHandle(windowId: number): Promise; openWindow(options?: IOpenEmptyWindowOptions): Promise; openWindow(toOpen: IWindowOpenable[], options?: IOpenWindowOptions): Promise; diff --git a/src/vs/platform/native/electron-main/nativeHostMainService.ts b/src/vs/platform/native/electron-main/nativeHostMainService.ts index c9a79ccf673ed..703f89ef66bd2 100644 --- a/src/vs/platform/native/electron-main/nativeHostMainService.ts +++ b/src/vs/platform/native/electron-main/nativeHostMainService.ts @@ -189,6 +189,14 @@ export class NativeHostMainService extends Disposable implements INativeHostMain return undefined; } + async getNativeWindowHandle(fallbackWindowId: number | undefined, windowId: number): Promise { + const window = this.windowById(windowId, fallbackWindowId); + if (window?.win) { + return VSBuffer.wrap(window.win.getNativeWindowHandle()); + } + return undefined; + } + openWindow(windowId: number | undefined, options?: IOpenEmptyWindowOptions): Promise; openWindow(windowId: number | undefined, toOpen: IWindowOpenable[], options?: IOpenWindowOptions): Promise; openWindow(windowId: number | undefined, arg1?: IOpenEmptyWindowOptions | IWindowOpenable[], arg2?: IOpenWindowOptions): Promise { diff --git a/src/vs/workbench/api/browser/mainThreadWindow.ts b/src/vs/workbench/api/browser/mainThreadWindow.ts index 29760af2182c4..393b182479bc2 100644 --- a/src/vs/workbench/api/browser/mainThreadWindow.ts +++ b/src/vs/workbench/api/browser/mainThreadWindow.ts @@ -11,6 +11,7 @@ import { extHostNamedCustomer, IExtHostContext } from '../../services/extensions import { ExtHostContext, ExtHostWindowShape, IOpenUriOptions, MainContext, MainThreadWindowShape } from '../common/extHost.protocol.js'; import { IHostService } from '../../services/host/browser/host.js'; import { IUserActivityService } from '../../services/userActivity/common/userActivityService.js'; +import { encodeBase64 } from '../../../base/common/buffer.js'; @extHostNamedCustomer(MainContext.MainThreadWindow) export class MainThreadWindow implements MainThreadWindowShape { @@ -29,12 +30,24 @@ export class MainThreadWindow implements MainThreadWindowShape { Event.latch(hostService.onDidChangeFocus) (this.proxy.$onDidChangeWindowFocus, this.proxy, this.disposables); userActivityService.onDidChangeIsActive(this.proxy.$onDidChangeWindowActive, this.proxy, this.disposables); + this.registerNativeHandle(); } dispose(): void { this.disposables.dispose(); } + registerNativeHandle(): void { + Event.latch(this.hostService.onDidChangeActiveWindow)( + async windowId => { + const handle = await this.hostService.getNativeWindowHandle(windowId); + this.proxy.$onDidChangeActiveNativeWindowHandle(handle ? encodeBase64(handle) : undefined); + }, + this, + this.disposables + ); + } + $getInitialState() { return Promise.resolve({ isFocused: this.hostService.hasFocus, diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index d57fd4541a7a4..f7e559d356a5c 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -2596,6 +2596,7 @@ export interface ExtHostDecorationsShape { export interface ExtHostWindowShape { $onDidChangeWindowFocus(value: boolean): void; $onDidChangeWindowActive(value: boolean): void; + $onDidChangeActiveNativeWindowHandle(handle: string | undefined): void; } export interface ExtHostLogLevelServiceShape { diff --git a/src/vs/workbench/api/common/extHostWindow.ts b/src/vs/workbench/api/common/extHostWindow.ts index e75364dc089cb..4303200183192 100644 --- a/src/vs/workbench/api/common/extHostWindow.ts +++ b/src/vs/workbench/api/common/extHostWindow.ts @@ -61,6 +61,10 @@ export class ExtHostWindow implements ExtHostWindowShape { return this._nativeHandle; } + $onDidChangeActiveNativeWindowHandle(handle: string | undefined): void { + this._nativeHandle = handle ? decodeBase64(handle).buffer : undefined; + } + $onDidChangeWindowFocus(value: boolean) { this.onDidChangeWindowProperty('focused', value); } diff --git a/src/vs/workbench/services/host/browser/browserHostService.ts b/src/vs/workbench/services/host/browser/browserHostService.ts index 97436b6913ef3..c2a2374c6c962 100644 --- a/src/vs/workbench/services/host/browser/browserHostService.ts +++ b/src/vs/workbench/services/host/browser/browserHostService.ts @@ -640,6 +640,13 @@ export class BrowserHostService extends Disposable implements IHostService { //#endregion + //#region Native Handle + + async getNativeWindowHandle(_windowId: number) { + return undefined; + } + + //#endregion } registerSingleton(IHostService, BrowserHostService, InstantiationType.Delayed); diff --git a/src/vs/workbench/services/host/browser/host.ts b/src/vs/workbench/services/host/browser/host.ts index 8f52dc3f20515..657c5ec4bad6e 100644 --- a/src/vs/workbench/services/host/browser/host.ts +++ b/src/vs/workbench/services/host/browser/host.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { VSBuffer } from '../../../../base/common/buffer.js'; import { Event } from '../../../../base/common/event.js'; import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; import { IWindowOpenable, IOpenWindowOptions, IOpenEmptyWindowOptions, IPoint, IRectangle } from '../../../../platform/window/common/window.js'; @@ -130,4 +131,13 @@ export interface IHostService { getScreenshot(): Promise; //#endregion + + //#region Native Handle + + /** + * Get the native handle of the window. + */ + getNativeWindowHandle(windowId: number): Promise; + + //#endregion } diff --git a/src/vs/workbench/services/host/electron-sandbox/nativeHostService.ts b/src/vs/workbench/services/host/electron-sandbox/nativeHostService.ts index 2e57a3c5ac361..8bdfe9743feaa 100644 --- a/src/vs/workbench/services/host/electron-sandbox/nativeHostService.ts +++ b/src/vs/workbench/services/host/electron-sandbox/nativeHostService.ts @@ -17,6 +17,7 @@ import { IMainProcessService } from '../../../../platform/ipc/common/mainProcess import { disposableWindowInterval, getActiveDocument, getWindowId, getWindowsCount, hasWindow, onDidRegisterWindow } from '../../../../base/browser/dom.js'; import { memoize } from '../../../../base/common/decorators.js'; import { isAuxiliaryWindow } from '../../../../base/browser/window.js'; +import { VSBuffer } from '../../../../base/common/buffer.js'; class WorkbenchNativeHostService extends NativeHostService { @@ -193,6 +194,18 @@ class WorkbenchHostService extends Disposable implements IHostService { } //#endregion + + //#region Native Handle + + private _nativeWindowHandleCache = new Map>(); + async getNativeWindowHandle(windowId: number): Promise { + if (!this._nativeWindowHandleCache.has(windowId)) { + this._nativeWindowHandleCache.set(windowId, this.nativeHostService.getNativeWindowHandle(windowId)); + } + return this._nativeWindowHandleCache.get(windowId)!; + } + + //#endregion } registerSingleton(IHostService, WorkbenchHostService, InstantiationType.Delayed); diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts index 4d822fd7726c0..4229f61ae24bd 100644 --- a/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -1570,6 +1570,8 @@ export class TestHostService implements IHostService { async getScreenshot(): Promise { return undefined; } + async getNativeWindowHandle(_windowId: number): Promise { return undefined; } + readonly colorScheme = ColorScheme.DARK; onDidChangeColorScheme = Event.None; } diff --git a/src/vs/workbench/test/electron-sandbox/workbenchTestServices.ts b/src/vs/workbench/test/electron-sandbox/workbenchTestServices.ts index 46b3d633b41ae..c58ce20646c2f 100644 --- a/src/vs/workbench/test/electron-sandbox/workbenchTestServices.ts +++ b/src/vs/workbench/test/electron-sandbox/workbenchTestServices.ts @@ -85,6 +85,7 @@ export class TestNativeHostService implements INativeHostService { async getWindows(): Promise { return []; } async getActiveWindowId(): Promise { return undefined; } async getActiveWindowPosition(): Promise { return undefined; } + async getNativeWindowHandle(windowId: number): Promise { return undefined; } openWindow(options?: IOpenEmptyWindowOptions): Promise; openWindow(toOpen: IWindowOpenable[], options?: IOpenWindowOptions): Promise; From f5452ff2a3e6816b6ddaadce6c28dd5687a1a894 Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt Date: Thu, 21 Nov 2024 17:20:12 -0800 Subject: [PATCH 053/119] Move `env.nativeHandle` to `window.nativeHandle` (#234395) --- .../microsoft-authentication/src/node/authProvider.ts | 6 +++--- src/vs/workbench/api/common/extHost.api.impl.ts | 8 ++++---- src/vscode-dts/vscode.proposed.nativeWindowHandle.d.ts | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/extensions/microsoft-authentication/src/node/authProvider.ts b/extensions/microsoft-authentication/src/node/authProvider.ts index f6c771e4f9ae4..af34273afa4d0 100644 --- a/extensions/microsoft-authentication/src/node/authProvider.ts +++ b/extensions/microsoft-authentication/src/node/authProvider.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { AccountInfo, AuthenticationResult, ServerError } from '@azure/msal-node'; -import { AuthenticationGetSessionOptions, AuthenticationProvider, AuthenticationProviderAuthenticationSessionsChangeEvent, AuthenticationProviderSessionOptions, AuthenticationSession, AuthenticationSessionAccountInformation, CancellationError, env, EventEmitter, ExtensionContext, l10n, LogOutputChannel, Memento, SecretStorage, Uri, window } from 'vscode'; +import { AuthenticationGetSessionOptions, AuthenticationProvider, AuthenticationProviderAuthenticationSessionsChangeEvent, AuthenticationProviderSessionOptions, AuthenticationSession, AuthenticationSessionAccountInformation, CancellationError, env, EventEmitter, ExtensionContext, l10n, LogOutputChannel, Uri, window } from 'vscode'; import { Environment } from '@azure/ms-rest-azure-env'; import { CachedPublicClientApplicationManager } from './publicClientCache'; import { UriHandlerLoopbackClient } from '../common/loopbackClientAndOpener'; @@ -190,7 +190,7 @@ export class MsalAuthProvider implements AuthenticationProvider { let result: AuthenticationResult | undefined; try { - const windowHandle = env.nativeHandle ? Buffer.from(env.nativeHandle) : undefined; + const windowHandle = window.nativeHandle ? Buffer.from(window.nativeHandle) : undefined; result = await cachedPca.acquireTokenInteractive({ openBrowser: async (url: string) => { await env.openExternal(Uri.parse(url)); }, scopes: scopeData.scopesToSend, @@ -232,7 +232,7 @@ export class MsalAuthProvider implements AuthenticationProvider { // The user wants to try the loopback client or we got an error likely due to spinning up the server const loopbackClient = new UriHandlerLoopbackClient(this._uriHandler, redirectUri, this._logger); try { - const windowHandle = env.nativeHandle ? Buffer.from(env.nativeHandle) : undefined; + const windowHandle = window.nativeHandle ? Buffer.from(window.nativeHandle) : undefined; result = await cachedPca.acquireTokenInteractive({ openBrowser: (url: string) => loopbackClient.openBrowser(url), scopes: scopeData.scopesToSend, diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 672848e05fe61..f8a27ed067514 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -436,10 +436,6 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I get appCommit(): string | undefined { checkProposedApiEnabled(extension, 'resolvers'); return initData.commit; - }, - get nativeHandle(): Uint8Array | undefined { - checkProposedApiEnabled(extension, 'nativeWindowHandle'); - return extHostWindow.nativeHandle; } }; if (!initData.environment.extensionTestsLocationURI) { @@ -927,6 +923,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I registerShareProvider(selector: vscode.DocumentSelector, provider: vscode.ShareProvider): vscode.Disposable { checkProposedApiEnabled(extension, 'shareProvider'); return extHostShare.registerShareProvider(checkSelector(selector), provider); + }, + get nativeHandle(): Uint8Array | undefined { + checkProposedApiEnabled(extension, 'nativeWindowHandle'); + return extHostWindow.nativeHandle; } }; diff --git a/src/vscode-dts/vscode.proposed.nativeWindowHandle.d.ts b/src/vscode-dts/vscode.proposed.nativeWindowHandle.d.ts index 592f3266e14a5..fa9c7732c0040 100644 --- a/src/vscode-dts/vscode.proposed.nativeWindowHandle.d.ts +++ b/src/vscode-dts/vscode.proposed.nativeWindowHandle.d.ts @@ -7,10 +7,10 @@ declare module 'vscode' { - export namespace env { + export namespace window { /** * Retrieves the native window handle of the current active window. - * The current active window may not be associated with this extension host. + * This will be updated when the active window changes. */ export const nativeHandle: Uint8Array | undefined; } From 019832a505b41fa6411f4ada73d8c044be3168f7 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Fri, 22 Nov 2024 10:15:29 +0100 Subject: [PATCH 054/119] Change working set title and excluded files color (#234406) --- .../chatContentParts/chatReferencesContentPart.ts | 12 +++++++----- .../workbench/contrib/chat/browser/chatInputPart.ts | 9 ++++----- src/vs/workbench/contrib/chat/browser/media/chat.css | 4 ++++ 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatReferencesContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatReferencesContentPart.ts index e05b69b29af3d..3b71a48f0cc90 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatReferencesContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatReferencesContentPart.ts @@ -375,17 +375,18 @@ class CollapsibleListRenderer implements IListRenderer e.kind === 'reference' && e.state === WorkingSetEntryState.Suggested).length; } - if (entries.length > 1) { - const fileCount = entries.length - suggestedFilesInWorkingSetCount; - overviewFileCount.textContent = ' ' + (fileCount === 1 ? localize('chatEditingSession.oneFile', '(1 file)') : localize('chatEditingSession.manyFiles', '({0} files)', fileCount)); - } if (excludedEntries.length > 0) { - overviewFileCount.textContent = ' ' + localize('chatEditingSession.excludedFiles', '({0} files, {1} excluded)', entries.length, excludedEntries.length); + overviewFileCount.textContent = ' ' + localize('chatEditingSession.excludedFiles', '({0}/{1} files)', this.chatEditingService.editingSessionFileLimit + excludedEntries.length, this.chatEditingService.editingSessionFileLimit); + } else if (entries.length > 1) { + const fileCount = entries.length - suggestedFilesInWorkingSetCount; + overviewFileCount.textContent = ' ' + (fileCount === 1 ? localize('chatEditingSession.oneFile', '(1 file)') : localize('chatEditingSession.manyFiles', '({0} files)', fileCount)); } const fileLimitReached = remainingFileEntriesBudget <= 0; diff --git a/src/vs/workbench/contrib/chat/browser/media/chat.css b/src/vs/workbench/contrib/chat/browser/media/chat.css index 35828792a4d45..d699d995740fd 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/media/chat.css @@ -1165,6 +1165,10 @@ have to be updated for changes to the rules above, or to support more deeply nes .monaco-icon-label { padding: 0px 3px; } + + .monaco-icon-label.excluded { + color: var(--vscode-notificationsWarningIcon-foreground) + } } .interactive-item-container .chat-notification-widget { From bb26c9f8b14f1f50b5f6badd9c5ea5649edd48b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Moreno?= Date: Fri, 22 Nov 2024 10:49:51 +0100 Subject: [PATCH 055/119] update esrp params (#234407) --- build/azure-pipelines/common/sign.js | 11 ++++++++++- build/azure-pipelines/common/sign.ts | 13 ++++++++++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/build/azure-pipelines/common/sign.js b/build/azure-pipelines/common/sign.js index 9c1c2493c6467..7b4b620d1fad8 100644 --- a/build/azure-pipelines/common/sign.js +++ b/build/azure-pipelines/common/sign.js @@ -126,6 +126,14 @@ function getParams(type) { function main([esrpCliPath, type, folderPath, pattern]) { const tmp = new Temp(); process.on('exit', () => tmp.dispose()); + const key = crypto.randomBytes(32); + const iv = crypto.randomBytes(16); + const encryptionDetailsPath = tmp.tmpNameSync(); + fs.writeFileSync(encryptionDetailsPath, JSON.stringify({ key: key.toString('hex'), iv: iv.toString('hex') })); + const cipher = crypto.createCipheriv('aes-256-cbc', key, iv); + const encryptedToken = cipher.update(process.env['SYSTEM_ACCESSTOKEN'].trim(), 'utf8', 'hex') + cipher.final('hex'); + const encryptedTokenPath = tmp.tmpNameSync(); + fs.writeFileSync(encryptedTokenPath, encryptedToken); const patternPath = tmp.tmpNameSync(); fs.writeFileSync(patternPath, pattern); const paramsPath = tmp.tmpNameSync(); @@ -142,7 +150,8 @@ function main([esrpCliPath, type, folderPath, pattern]) { managedIdentityTenantId: process.env['VSCODE_ESRP_TENANT_ID'], serviceConnectionId: process.env['VSCODE_ESRP_SERVICE_CONNECTION_ID'], tempDirectory: os.tmpdir(), - systemAccessToken: process.env['SYSTEM_ACCESSTOKEN'] + systemAccessToken: encryptedTokenPath, + encryptionKey: encryptionDetailsPath }; const args = [ esrpCliPath, diff --git a/build/azure-pipelines/common/sign.ts b/build/azure-pipelines/common/sign.ts index b40f3cb61071e..df8e26ff9218c 100644 --- a/build/azure-pipelines/common/sign.ts +++ b/build/azure-pipelines/common/sign.ts @@ -138,6 +138,16 @@ export function main([esrpCliPath, type, folderPath, pattern]: string[]) { const tmp = new Temp(); process.on('exit', () => tmp.dispose()); + const key = crypto.randomBytes(32); + const iv = crypto.randomBytes(16); + const encryptionDetailsPath = tmp.tmpNameSync(); + fs.writeFileSync(encryptionDetailsPath, JSON.stringify({ key: key.toString('hex'), iv: iv.toString('hex') })); + + const cipher = crypto.createCipheriv('aes-256-cbc', key, iv); + const encryptedToken = cipher.update(process.env['SYSTEM_ACCESSTOKEN']!.trim(), 'utf8', 'hex') + cipher.final('hex'); + const encryptedTokenPath = tmp.tmpNameSync(); + fs.writeFileSync(encryptedTokenPath, encryptedToken); + const patternPath = tmp.tmpNameSync(); fs.writeFileSync(patternPath, pattern); @@ -157,7 +167,8 @@ export function main([esrpCliPath, type, folderPath, pattern]: string[]) { managedIdentityTenantId: process.env['VSCODE_ESRP_TENANT_ID'], serviceConnectionId: process.env['VSCODE_ESRP_SERVICE_CONNECTION_ID'], tempDirectory: os.tmpdir(), - systemAccessToken: process.env['SYSTEM_ACCESSTOKEN'] + systemAccessToken: encryptedTokenPath, + encryptionKey: encryptionDetailsPath }; const args = [ From 3ef70d7b55ccd58b3390c30ca005ca8502e188fa Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Fri, 22 Nov 2024 11:31:37 +0100 Subject: [PATCH 056/119] Git Blame - switch to LineRangeMapping and exclusive line ranges (#234409) Switch to LineRangeMapping and exclusive line ranges --- extensions/git/src/blame.ts | 158 +++++++++--------- src/vs/editor/common/diff/rangeMapping.ts | 27 +++ src/vs/platform/editor/common/editor.ts | 4 +- .../api/browser/mainThreadEditors.ts | 20 ++- .../workbench/api/common/extHost.protocol.ts | 4 +- .../api/common/extHostTextEditors.ts | 26 ++- .../browser/parts/editor/editorCommands.ts | 26 +-- ...de.proposed.textEditorDiffInformation.d.ts | 12 +- 8 files changed, 163 insertions(+), 114 deletions(-) diff --git a/extensions/git/src/blame.ts b/extensions/git/src/blame.ts index fc0f74fcf169a..2a2c7c65396d3 100644 --- a/extensions/git/src/blame.ts +++ b/extensions/git/src/blame.ts @@ -3,56 +3,63 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { DecorationOptions, l10n, Position, Range, TextEditor, TextEditorChange, TextEditorDecorationType, TextEditorChangeKind, ThemeColor, Uri, window, workspace, EventEmitter, ConfigurationChangeEvent, StatusBarItem, StatusBarAlignment, Command, MarkdownString, commands, LineChange } from 'vscode'; +import { DecorationOptions, l10n, Position, Range, TextEditor, TextEditorChange, TextEditorDecorationType, TextEditorChangeKind, ThemeColor, Uri, window, workspace, EventEmitter, ConfigurationChangeEvent, StatusBarItem, StatusBarAlignment, Command, MarkdownString, commands } from 'vscode'; import { Model } from './model'; import { dispose, fromNow, IDisposable, pathEquals } from './util'; import { Repository } from './repository'; import { throttle } from './decorators'; import { BlameInformation } from './git'; -function isLineChanged(lineNumber: number, changes: readonly TextEditorChange[]): boolean { - for (const change of changes) { - // If the change is a delete, skip it - if (change.kind === TextEditorChangeKind.Deletion) { - continue; - } +function lineRangesContainLine(changes: readonly TextEditorChange[], lineNumber: number): boolean { + return changes.some(c => c.modified.startLineNumber <= lineNumber && lineNumber < c.modified.endLineNumberExclusive); +} - const startLineNumber = change.modifiedStartLineNumber; - const endLineNumber = change.modifiedEndLineNumber || startLineNumber; - if (lineNumber >= startLineNumber && lineNumber <= endLineNumber) { - return true; - } +function lineRangeLength(startLineNumber: number, endLineNumberExclusive: number): number { + return endLineNumberExclusive - startLineNumber; +} + +function toTextEditorChange(originalStartLineNumber: number, originalEndLineNumberExclusive: number, modifiedStartLineNumber: number, modifiedEndLineNumberExclusive: number): TextEditorChange { + let kind: TextEditorChangeKind; + if (originalStartLineNumber === originalEndLineNumberExclusive) { + kind = TextEditorChangeKind.Addition; + } else if (modifiedStartLineNumber === modifiedEndLineNumberExclusive) { + kind = TextEditorChangeKind.Deletion; + } else { + kind = TextEditorChangeKind.Modification; } - return false; + return { + original: { startLineNumber: originalStartLineNumber, endLineNumberExclusive: originalEndLineNumberExclusive }, + modified: { startLineNumber: modifiedStartLineNumber, endLineNumberExclusive: modifiedEndLineNumberExclusive }, + kind + }; } -function mapLineNumber(lineNumber: number, changes: readonly TextEditorChange[]): number { +function mapModifiedLineNumberToOriginalLineNumber(lineNumber: number, changes: readonly TextEditorChange[]): number { if (changes.length === 0) { return lineNumber; } for (const change of changes) { - // Line number is before the change so there is not need to process further - if ((change.kind === TextEditorChangeKind.Addition && lineNumber < change.modifiedStartLineNumber) || - (change.kind === TextEditorChangeKind.Modification && lineNumber < change.modifiedStartLineNumber) || - (change.kind === TextEditorChangeKind.Deletion && lineNumber < change.originalStartLineNumber)) { + // Do not process changes after the line number + if (lineNumber < change.modified.startLineNumber) { break; } // Map line number to the original line number if (change.kind === TextEditorChangeKind.Addition) { // Addition - lineNumber = lineNumber - (change.modifiedEndLineNumber - change.originalStartLineNumber); + lineNumber = lineNumber - lineRangeLength(change.modified.startLineNumber, change.modified.endLineNumberExclusive); } else if (change.kind === TextEditorChangeKind.Deletion) { // Deletion - lineNumber = lineNumber + (change.originalEndLineNumber - change.originalStartLineNumber) + 1; + lineNumber = lineNumber + lineRangeLength(change.original.startLineNumber, change.original.endLineNumberExclusive); } else if (change.kind === TextEditorChangeKind.Modification) { // Modification - const originalLineCount = change.originalEndLineNumber - change.originalStartLineNumber + 1; - const modifiedLineCount = change.modifiedEndLineNumber - change.modifiedStartLineNumber + 1; - if (originalLineCount !== modifiedLineCount) { - lineNumber = lineNumber - (modifiedLineCount - originalLineCount); + const originalRangeLength = lineRangeLength(change.original.startLineNumber, change.original.endLineNumberExclusive); + const modifiedRangeLength = lineRangeLength(change.modified.startLineNumber, change.modified.endLineNumberExclusive); + + if (originalRangeLength !== modifiedRangeLength) { + lineNumber = lineNumber - (modifiedRangeLength - originalRangeLength); } } else { throw new Error('Unexpected change kind'); @@ -62,41 +69,6 @@ function mapLineNumber(lineNumber: number, changes: readonly TextEditorChange[]) return lineNumber; } -function getBlameInformationHover(documentUri: Uri, blameInformation: BlameInformation | string): MarkdownString { - if (typeof blameInformation === 'string') { - return new MarkdownString(blameInformation, true); - } - - const markdownString = new MarkdownString(); - markdownString.supportThemeIcons = true; - markdownString.isTrusted = true; - - if (blameInformation.authorName) { - markdownString.appendMarkdown(`$(account) **${blameInformation.authorName}**`); - - if (blameInformation.date) { - const dateString = new Date(blameInformation.date).toLocaleString(undefined, { year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric' }); - markdownString.appendMarkdown(`, $(history) ${fromNow(blameInformation.date, true, true)} (${dateString})`); - } - - markdownString.appendMarkdown('\n\n'); - } - - markdownString.appendMarkdown(`${blameInformation.message}\n\n`); - markdownString.appendMarkdown(`---\n\n`); - - markdownString.appendMarkdown(`[$(eye) View Commit](command:git.blameStatusBarItem.viewCommit?${encodeURIComponent(JSON.stringify([documentUri, blameInformation.id]))})`); - markdownString.appendMarkdown('  |  '); - markdownString.appendMarkdown(`[$(copy) ${blameInformation.id.substring(0, 8)}](command:git.blameStatusBarItem.copyContent?${encodeURIComponent(JSON.stringify(blameInformation.id))})`); - - if (blameInformation.message) { - markdownString.appendMarkdown('  '); - markdownString.appendMarkdown(`[$(copy) Message](command:git.blameStatusBarItem.copyContent?${encodeURIComponent(JSON.stringify(blameInformation.message))})`); - } - - return markdownString; -} - interface RepositoryBlameInformation { readonly commit: string; /* commit used for blame information */ readonly blameInformation: Map; @@ -132,6 +104,41 @@ export class GitBlameController { this._updateTextEditorBlameInformation(window.activeTextEditor); } + getBlameInformationHover(documentUri: Uri, blameInformation: BlameInformation | string): MarkdownString { + if (typeof blameInformation === 'string') { + return new MarkdownString(blameInformation, true); + } + + const markdownString = new MarkdownString(); + markdownString.supportThemeIcons = true; + markdownString.isTrusted = true; + + if (blameInformation.authorName) { + markdownString.appendMarkdown(`$(account) **${blameInformation.authorName}**`); + + if (blameInformation.date) { + const dateString = new Date(blameInformation.date).toLocaleString(undefined, { year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric' }); + markdownString.appendMarkdown(`, $(history) ${fromNow(blameInformation.date, true, true)} (${dateString})`); + } + + markdownString.appendMarkdown('\n\n'); + } + + markdownString.appendMarkdown(`${blameInformation.message}\n\n`); + markdownString.appendMarkdown(`---\n\n`); + + markdownString.appendMarkdown(`[$(eye) View Commit](command:git.blameStatusBarItem.viewCommit?${encodeURIComponent(JSON.stringify([documentUri, blameInformation.id]))})`); + markdownString.appendMarkdown('  |  '); + markdownString.appendMarkdown(`[$(copy) ${blameInformation.id.substring(0, 8)}](command:git.blameStatusBarItem.copyContent?${encodeURIComponent(JSON.stringify(blameInformation.id))})`); + + if (blameInformation.message) { + markdownString.appendMarkdown('  '); + markdownString.appendMarkdown(`[$(copy) Message](command:git.blameStatusBarItem.copyContent?${encodeURIComponent(JSON.stringify(blameInformation.message))})`); + } + + return markdownString; + } + private _onDidOpenRepository(repository: Repository): void { const repositoryDisposables: IDisposable[] = []; @@ -222,24 +229,12 @@ export class GitBlameController { } // Get the diff information for the staged resource - const diffInformation: LineChange[] = await commands.executeCommand('_workbench.internal.computeDirtyDiff', resource.leftUri, resource.rightUri); + const diffInformation: [number, number, number, number][] = await commands.executeCommand('_workbench.internal.computeDiff', resource.leftUri, resource.rightUri); if (!diffInformation) { return undefined; } - changes = diffInformation.map(change => { - const kind = change.originalEndLineNumber === 0 ? TextEditorChangeKind.Addition : - change.modifiedEndLineNumber === 0 ? TextEditorChangeKind.Deletion : TextEditorChangeKind.Modification; - - return { - originalStartLineNumber: change.originalStartLineNumber, - originalEndLineNumber: change.originalEndLineNumber, - modifiedStartLineNumber: change.modifiedStartLineNumber, - modifiedEndLineNumber: change.modifiedEndLineNumber, - kind - } satisfies TextEditorChange; - }); - + changes = diffInformation.map(change => toTextEditorChange(change[0], change[1], change[2], change[3])); this._stagedResourceDiffInformation.set(repository, diffInformationMap.set(resource.resourceUri, changes)); return changes; @@ -261,16 +256,21 @@ export class GitBlameController { // to get the staged changes and if present, merge them with the diff information. const diffInformationStagedResources: TextEditorChange[] = await this._getStagedResourceDiffInformation(textEditor.document.uri) ?? []; + console.log('diffInformation', diffInformation.changes); + console.log('diffInformationStagedResources', diffInformationStagedResources); + + console.log('resourceBlameInformation', resourceBlameInformation); + const lineBlameInformation: LineBlameInformation[] = []; for (const lineNumber of textEditor.selections.map(s => s.active.line)) { // Check if the line is contained in the diff information - if (isLineChanged(lineNumber + 1, diffInformation.changes)) { + if (lineRangesContainLine(diffInformation.changes, lineNumber + 1)) { lineBlameInformation.push({ lineNumber, blameInformation: l10n.t('Not Committed Yet') }); continue; } // Check if the line is contained in the staged resources diff information - if (isLineChanged(lineNumber + 1, diffInformationStagedResources)) { + if (lineRangesContainLine(diffInformationStagedResources, lineNumber + 1)) { lineBlameInformation.push({ lineNumber, blameInformation: l10n.t('Not Committed Yet (Staged)') }); continue; } @@ -278,7 +278,7 @@ export class GitBlameController { const diffInformationAll = [...diffInformation.changes, ...diffInformationStagedResources]; // Map the line number to the git blame ranges using the diff information - const lineNumberWithDiff = mapLineNumber(lineNumber + 1, diffInformationAll); + const lineNumberWithDiff = mapModifiedLineNumberToOriginalLineNumber(lineNumber + 1, diffInformationAll); const blameInformation = resourceBlameInformation.find(blameInformation => { return blameInformation.ranges.find(range => { return lineNumberWithDiff >= range.startLineNumber && lineNumberWithDiff <= range.endLineNumber; @@ -356,7 +356,7 @@ class GitBlameEditorDecoration { const contentText = typeof blame.blameInformation === 'string' ? blame.blameInformation : `${blame.blameInformation.message ?? ''}, ${blame.blameInformation.authorName ?? ''} (${fromNow(blame.blameInformation.date ?? Date.now(), true, true)})`; - const hoverMessage = getBlameInformationHover(textEditor.document.uri, blame.blameInformation); + const hoverMessage = this._controller.getBlameInformationHover(textEditor.document.uri, blame.blameInformation); return this._createDecoration(blame.lineNumber, contentText, hoverMessage); }); @@ -446,11 +446,11 @@ class GitBlameStatusBarItem { if (typeof blameInformation[0].blameInformation === 'string') { this._statusBarItem.text = `$(git-commit) ${blameInformation[0].blameInformation}`; - this._statusBarItem.tooltip = getBlameInformationHover(textEditor.document.uri, blameInformation[0].blameInformation); + this._statusBarItem.tooltip = this._controller.getBlameInformationHover(textEditor.document.uri, blameInformation[0].blameInformation); this._statusBarItem.command = undefined; } else { this._statusBarItem.text = `$(git-commit) ${blameInformation[0].blameInformation.authorName ?? ''} (${fromNow(blameInformation[0].blameInformation.date ?? new Date(), true, true)})`; - this._statusBarItem.tooltip = getBlameInformationHover(textEditor.document.uri, blameInformation[0].blameInformation); + this._statusBarItem.tooltip = this._controller.getBlameInformationHover(textEditor.document.uri, blameInformation[0].blameInformation); this._statusBarItem.command = { title: l10n.t('View Commit'), command: 'git.blameStatusBarItem.viewCommit', diff --git a/src/vs/editor/common/diff/rangeMapping.ts b/src/vs/editor/common/diff/rangeMapping.ts index 80c18b9eddf40..09021d118b747 100644 --- a/src/vs/editor/common/diff/rangeMapping.ts +++ b/src/vs/editor/common/diff/rangeMapping.ts @@ -10,6 +10,7 @@ import { LineRange } from '../core/lineRange.js'; import { Position } from '../core/position.js'; import { Range } from '../core/range.js'; import { AbstractText, SingleTextEdit, TextEdit } from '../core/textEdit.js'; +import { IChange } from './legacyLinesDiffComputer.js'; /** * Maps a line range in the original text model to a line range in the modified text model. @@ -381,3 +382,29 @@ export function getLineRangeMapping(rangeMapping: RangeMapping, originalLines: A return new DetailedLineRangeMapping(originalLineRange, modifiedLineRange, [rangeMapping]); } + +export function lineRangeMappingFromChanges(changes: IChange[]): LineRangeMapping[] { + const lineRangeMapping: LineRangeMapping[] = []; + + for (const change of changes) { + let originalRange: LineRange; + if (change.originalEndLineNumber === 0) { + // Insertion + originalRange = new LineRange(change.originalStartLineNumber + 1, change.originalStartLineNumber + 1); + } else { + originalRange = new LineRange(change.originalStartLineNumber, change.originalEndLineNumber + 1); + } + + let modifiedRange: LineRange; + if (change.modifiedEndLineNumber === 0) { + // Deletion + modifiedRange = new LineRange(change.modifiedStartLineNumber + 1, change.modifiedStartLineNumber + 1); + } else { + modifiedRange = new LineRange(change.modifiedStartLineNumber, change.modifiedEndLineNumber + 1); + } + + lineRangeMapping.push(new LineRangeMapping(originalRange, modifiedRange)); + } + + return lineRangeMapping; +} diff --git a/src/vs/platform/editor/common/editor.ts b/src/vs/platform/editor/common/editor.ts index 1443316db59d7..0138721fb1d9b 100644 --- a/src/vs/platform/editor/common/editor.ts +++ b/src/vs/platform/editor/common/editor.ts @@ -381,9 +381,9 @@ export interface ITextEditorOptions extends IEditorOptions { export type ITextEditorChange = [ originalStartLineNumber: number, - originalEndLineNumber: number, + originalEndLineNumberExclusive: number, modifiedStartLineNumber: number, - modifiedEndLineNumber: number + modifiedEndLineNumberExclusive: number ]; export interface ITextEditorDiffInformation { diff --git a/src/vs/workbench/api/browser/mainThreadEditors.ts b/src/vs/workbench/api/browser/mainThreadEditors.ts index 56abf0cb28acc..93b9331889d18 100644 --- a/src/vs/workbench/api/browser/mainThreadEditors.ts +++ b/src/vs/workbench/api/browser/mainThreadEditors.ts @@ -33,6 +33,7 @@ import { IDirtyDiffModelService } from '../../contrib/scm/browser/diff.js'; import { autorun, constObservable, derived, derivedOpts, IObservable, observableFromEvent } from '../../../base/common/observable.js'; import { IUriIdentityService } from '../../../platform/uriIdentity/common/uriIdentity.js'; import { isITextModel } from '../../../editor/common/model.js'; +import { LineRangeMapping, lineRangeMappingFromChanges } from '../../../editor/common/diff/rangeMapping.js'; export interface IMainThreadEditorLocator { getEditor(id: string): MainThreadTextEditor | undefined; @@ -144,7 +145,7 @@ export class MainThreadTextEditors implements MainThreadTextEditorsShape { ? observableFromEvent(this, diffEditor.onDidChangeModel, () => diffEditor.getModel()) : observableFromEvent(this, codeEditor.onDidChangeModel, () => codeEditor.getModel()); - const editorChangesObs = derived>(reader => { + const editorChangesObs = derived>(reader => { const editorModel = editorModelObs.read(reader); if (!editorModel) { return constObservable(undefined); @@ -153,10 +154,12 @@ export class MainThreadTextEditors implements MainThreadTextEditorsShape { // DiffEditor if (!isITextModel(editorModel)) { return observableFromEvent(diffEditor.onDidUpdateDiff, () => { + const changes = diffEditor.getDiffComputationResult()?.changes2 ?? []; + return { original: editorModel.original.uri, modified: editorModel.modified.uri, - changes: diffEditor.getLineChanges() ?? [] + changes: changes.map(change => change as LineRangeMapping) }; }); } @@ -177,10 +180,13 @@ export class MainThreadTextEditors implements MainThreadTextEditorsShape { .filter(change => change.label === scmQuickDiff.label) .map(change => change.change); + // Convert IChange[] to LineRangeMapping[] + const lineRangeMapping = lineRangeMappingFromChanges(changes); + return { original: scmQuickDiff.originalResource, modified: editorModel.uri, - changes + changes: lineRangeMapping }; }); }); @@ -201,10 +207,10 @@ export class MainThreadTextEditors implements MainThreadTextEditorsShape { const changes: ITextEditorChange[] = editorChanges.changes .map(change => [ - change.originalStartLineNumber, - change.originalEndLineNumber, - change.modifiedStartLineNumber, - change.modifiedEndLineNumber + change.original.startLineNumber, + change.original.endLineNumberExclusive, + change.modified.startLineNumber, + change.modified.endLineNumberExclusive ]); return { diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index f7e559d356a5c..4fb99e4aed94f 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -1823,9 +1823,9 @@ export interface ITextEditorPositionData { export type ITextEditorChange = [ originalStartLineNumber: number, - originalEndLineNumber: number, + originalEndLineNumberExclusive: number, modifiedStartLineNumber: number, - modifiedEndLineNumber: number + modifiedEndLineNumberExclusive: number ]; export interface ITextEditorDiffInformation { diff --git a/src/vs/workbench/api/common/extHostTextEditors.ts b/src/vs/workbench/api/common/extHostTextEditors.ts index f158d5aff1f96..7764ac504c239 100644 --- a/src/vs/workbench/api/common/extHostTextEditors.ts +++ b/src/vs/workbench/api/common/extHostTextEditors.ts @@ -179,16 +179,26 @@ export class ExtHostEditors extends Disposable implements ExtHostEditorsShape { const modified = URI.revive(diffInformation.modified); const changes = diffInformation.changes.map(change => { - const [originalStartLineNumber, originalEndLineNumber, modifiedStartLineNumber, modifiedEndLineNumber] = change; - - const kind = originalEndLineNumber === 0 ? TextEditorChangeKind.Addition : - modifiedEndLineNumber === 0 ? TextEditorChangeKind.Deletion : TextEditorChangeKind.Modification; + const [originalStartLineNumber, originalEndLineNumberExclusive, modifiedStartLineNumber, modifiedEndLineNumberExclusive] = change; + + let kind: vscode.TextEditorChangeKind; + if (originalStartLineNumber === originalEndLineNumberExclusive) { + kind = TextEditorChangeKind.Addition; + } else if (modifiedStartLineNumber === modifiedEndLineNumberExclusive) { + kind = TextEditorChangeKind.Deletion; + } else { + kind = TextEditorChangeKind.Modification; + } return { - originalStartLineNumber, - originalEndLineNumber, - modifiedStartLineNumber, - modifiedEndLineNumber, + original: { + startLineNumber: originalStartLineNumber, + endLineNumberExclusive: originalEndLineNumberExclusive + }, + modified: { + startLineNumber: modifiedStartLineNumber, + endLineNumberExclusive: modifiedEndLineNumberExclusive + }, kind } satisfies vscode.TextEditorChange; }); diff --git a/src/vs/workbench/browser/parts/editor/editorCommands.ts b/src/vs/workbench/browser/parts/editor/editorCommands.ts index 0ab2d529df23d..b22b9bc033aad 100644 --- a/src/vs/workbench/browser/parts/editor/editorCommands.ts +++ b/src/vs/workbench/browser/parts/editor/editorCommands.ts @@ -539,7 +539,7 @@ function registerOpenEditorAPICommands(): void { }); }); - CommandsRegistry.registerCommand('_workbench.internal.computeDirtyDiff', async (accessor: ServicesAccessor, original: UriComponents, modified: UriComponents) => { + CommandsRegistry.registerCommand('_workbench.internal.computeDiff', async (accessor: ServicesAccessor, original: UriComponents, modified: UriComponents) => { const configurationService = accessor.get(IConfigurationService); const editorWorkerService = accessor.get(IEditorWorkerService); const textModelService = accessor.get(ITextModelService); @@ -547,25 +547,27 @@ function registerOpenEditorAPICommands(): void { const originalResource = URI.revive(original); const modifiedResource = URI.revive(modified); - const originalModel = await textModelService.createModelReference(originalResource); - const modifiedModel = await textModelService.createModelReference(modifiedResource); - - const canComputeDirtyDiff = editorWorkerService.canComputeDirtyDiff(originalResource, modifiedResource); - if (!canComputeDirtyDiff) { - return undefined; - } + const originalTextModel = await textModelService.createModelReference(originalResource); + const modifiedTextModel = await textModelService.createModelReference(modifiedResource); const ignoreTrimWhitespaceSetting = configurationService.getValue<'true' | 'false' | 'inherit'>('scm.diffDecorationsIgnoreTrimWhitespace'); const ignoreTrimWhitespace = ignoreTrimWhitespaceSetting === 'inherit' ? configurationService.getValue('diffEditor.ignoreTrimWhitespace') : ignoreTrimWhitespaceSetting !== 'false'; - const changes = await editorWorkerService.computeDirtyDiff(originalResource, modifiedResource, ignoreTrimWhitespace); + const changes = await editorWorkerService.computeDiff(originalResource, modifiedResource, { + computeMoves: false, + ignoreTrimWhitespace, + maxComputationTimeMs: Number.MAX_SAFE_INTEGER + }, 'legacy'); - originalModel.dispose(); - modifiedModel.dispose(); + originalTextModel.dispose(); + modifiedTextModel.dispose(); - return changes; + return changes?.changes.map(c => [ + c.original.startLineNumber, c.original.endLineNumberExclusive, + c.modified.startLineNumber, c.modified.endLineNumberExclusive + ]); }); } diff --git a/src/vscode-dts/vscode.proposed.textEditorDiffInformation.d.ts b/src/vscode-dts/vscode.proposed.textEditorDiffInformation.d.ts index 1ca03446583d8..d737a5c2ef144 100644 --- a/src/vscode-dts/vscode.proposed.textEditorDiffInformation.d.ts +++ b/src/vscode-dts/vscode.proposed.textEditorDiffInformation.d.ts @@ -13,10 +13,14 @@ declare module 'vscode' { } export interface TextEditorChange { - readonly originalStartLineNumber: number; - readonly originalEndLineNumber: number; - readonly modifiedStartLineNumber: number; - readonly modifiedEndLineNumber: number; + readonly original: { + readonly startLineNumber: number; + readonly endLineNumberExclusive: number; + }; + readonly modified: { + readonly startLineNumber: number; + readonly endLineNumberExclusive: number; + }; readonly kind: TextEditorChangeKind; } From e0d9b269607a896cadbc1fba9a2d8a63effd71dc Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Fri, 22 Nov 2024 13:55:01 +0100 Subject: [PATCH 057/119] Implement allowed extensions feature (#234414) * implement allowed list support for extensions #84756 * undo change in settings * update setting desc * feedback: - specify publisher name without * - support listing versions with target platforms * change to release * improve setting description * add tests * add more tests --- src/vs/base/common/product.ts | 2 + .../sharedProcess/sharedProcessMain.ts | 4 +- src/vs/code/node/cliProcessMain.ts | 4 +- .../abstractExtensionManagementService.ts | 91 +++++-- .../common/allowedExtensionsService.ts | 136 ++++++++++ .../common/extensionGalleryService.ts | 41 ++- .../common/extensionManagement.ts | 11 +- .../common/extensionManagementIpc.ts | 26 +- .../common/extensionManagementUtil.ts | 10 +- .../common/extensionsScannerService.ts | 5 +- .../node/extensionManagementService.ts | 6 +- .../common/allowedExtensionsService.test.ts | 233 ++++++++++++++++++ .../extensionsProfileScannerService.test.ts | 1 + .../platform/extensions/common/extensions.ts | 2 + .../node/remoteExtensionHostAgentCli.ts | 4 +- src/vs/server/node/serverServices.ts | 4 +- .../api/test/browser/extHostTelemetry.test.ts | 1 + .../common/extHostExtensionActivator.test.ts | 1 + .../contrib/debug/test/node/debugger.test.ts | 3 + .../browser/extensions.contribution.ts | 76 +++++- .../extensions/browser/extensionsActions.ts | 24 +- .../browser/extensionsWorkbenchService.ts | 20 +- .../extensionRecommendationsService.test.ts | 2 +- .../extensionsActions.test.ts | 4 +- .../electron-sandbox/extensionsViews.test.ts | 2 +- .../extensionsWorkbenchService.test.ts | 4 +- .../builtinExtensionsScannerService.ts | 3 +- .../browser/extensionEnablementService.ts | 31 ++- .../browser/webExtensionsScannerService.ts | 3 +- .../common/extensionManagement.ts | 1 + .../extensionManagementChannelClient.ts | 5 +- .../remoteExtensionManagementService.ts | 4 +- .../common/webExtensionManagementService.ts | 19 +- .../nativeExtensionManagementService.ts | 5 +- .../remoteExtensionManagementService.ts | 5 +- .../extensionEnablementService.test.ts | 13 +- .../services/extensions/common/extensions.ts | 7 +- .../extensionDescriptionRegistry.test.ts | 1 + .../test/browser/workbenchTestServices.ts | 2 + src/vs/workbench/workbench.common.main.ts | 4 +- 40 files changed, 709 insertions(+), 111 deletions(-) create mode 100644 src/vs/platform/extensionManagement/common/allowedExtensionsService.ts create mode 100644 src/vs/platform/extensionManagement/test/common/allowedExtensionsService.test.ts diff --git a/src/vs/base/common/product.ts b/src/vs/base/common/product.ts index fcb54c94eb396..90b650e29539b 100644 --- a/src/vs/base/common/product.ts +++ b/src/vs/base/common/product.ts @@ -105,6 +105,8 @@ export interface IProductConfiguration { readonly nlsBaseUrl: string; }; + readonly extensionPublisherMappings?: IStringDictionary; + readonly extensionRecommendations?: IStringDictionary; readonly configBasedExtensionTips?: IStringDictionary; readonly exeBasedExtensionTips?: IStringDictionary; diff --git a/src/vs/code/electron-utility/sharedProcess/sharedProcessMain.ts b/src/vs/code/electron-utility/sharedProcess/sharedProcessMain.ts index 00a0b29125adb..563f563455a05 100644 --- a/src/vs/code/electron-utility/sharedProcess/sharedProcessMain.ts +++ b/src/vs/code/electron-utility/sharedProcess/sharedProcessMain.ts @@ -29,7 +29,7 @@ import { DownloadService } from '../../../platform/download/common/downloadServi import { INativeEnvironmentService } from '../../../platform/environment/common/environment.js'; import { GlobalExtensionEnablementService } from '../../../platform/extensionManagement/common/extensionEnablementService.js'; import { ExtensionGalleryService } from '../../../platform/extensionManagement/common/extensionGalleryService.js'; -import { IExtensionGalleryService, IExtensionManagementService, IExtensionTipsService, IGlobalExtensionEnablementService } from '../../../platform/extensionManagement/common/extensionManagement.js'; +import { IAllowedExtensionsService, IExtensionGalleryService, IExtensionManagementService, IExtensionTipsService, IGlobalExtensionEnablementService } from '../../../platform/extensionManagement/common/extensionManagement.js'; import { ExtensionSignatureVerificationService, IExtensionSignatureVerificationService } from '../../../platform/extensionManagement/node/extensionSignatureVerificationService.js'; import { ExtensionManagementChannel, ExtensionTipsChannel } from '../../../platform/extensionManagement/common/extensionManagementIpc.js'; import { ExtensionManagementService, INativeServerExtensionManagementService } from '../../../platform/extensionManagement/node/extensionManagementService.js'; @@ -119,6 +119,7 @@ import { getDesktopEnvironment } from '../../../base/common/desktopEnvironmentIn import { getCodeDisplayProtocol, getDisplayProtocol } from '../../../base/node/osDisplayProtocolInfo.js'; import { RequestService } from '../../../platform/request/electron-utility/requestService.js'; import { DefaultExtensionsInitializer } from './contrib/defaultExtensionsInitializer.js'; +import { AllowedExtensionsService } from '../../../platform/extensionManagement/common/allowedExtensionsService.js'; class SharedProcessMain extends Disposable implements IClientConnectionFilter { @@ -329,6 +330,7 @@ class SharedProcessMain extends Disposable implements IClientConnectionFilter { services.set(IExtensionsProfileScannerService, new SyncDescriptor(ExtensionsProfileScannerService, undefined, true)); services.set(IExtensionsScannerService, new SyncDescriptor(ExtensionsScannerService, undefined, true)); services.set(IExtensionSignatureVerificationService, new SyncDescriptor(ExtensionSignatureVerificationService, undefined, true)); + services.set(IAllowedExtensionsService, new SyncDescriptor(AllowedExtensionsService, undefined, true)); services.set(INativeServerExtensionManagementService, new SyncDescriptor(ExtensionManagementService, undefined, true)); // Extension Gallery diff --git a/src/vs/code/node/cliProcessMain.ts b/src/vs/code/node/cliProcessMain.ts index d3b20ecea47c7..decb23f830f99 100644 --- a/src/vs/code/node/cliProcessMain.ts +++ b/src/vs/code/node/cliProcessMain.ts @@ -22,7 +22,7 @@ import { NativeParsedArgs } from '../../platform/environment/common/argv.js'; import { INativeEnvironmentService } from '../../platform/environment/common/environment.js'; import { NativeEnvironmentService } from '../../platform/environment/node/environmentService.js'; import { ExtensionGalleryServiceWithNoStorageService } from '../../platform/extensionManagement/common/extensionGalleryService.js'; -import { IExtensionGalleryService, InstallOptions } from '../../platform/extensionManagement/common/extensionManagement.js'; +import { IAllowedExtensionsService, IExtensionGalleryService, InstallOptions } from '../../platform/extensionManagement/common/extensionManagement.js'; import { ExtensionSignatureVerificationService, IExtensionSignatureVerificationService } from '../../platform/extensionManagement/node/extensionSignatureVerificationService.js'; import { ExtensionManagementCLI } from '../../platform/extensionManagement/common/extensionManagementCLI.js'; import { IExtensionsProfileScannerService } from '../../platform/extensionManagement/common/extensionsProfileScannerService.js'; @@ -64,6 +64,7 @@ import { LoggerService } from '../../platform/log/node/loggerService.js'; import { localize } from '../../nls.js'; import { FileUserDataProvider } from '../../platform/userData/common/fileUserDataProvider.js'; import { addUNCHostToAllowlist, getUNCHost } from '../../base/node/unc.js'; +import { AllowedExtensionsService } from '../../platform/extensionManagement/common/allowedExtensionsService.js'; class CliMain extends Disposable { @@ -205,6 +206,7 @@ class CliMain extends Disposable { services.set(IExtensionsProfileScannerService, new SyncDescriptor(ExtensionsProfileScannerService, undefined, true)); services.set(IExtensionsScannerService, new SyncDescriptor(ExtensionsScannerService, undefined, true)); services.set(IExtensionSignatureVerificationService, new SyncDescriptor(ExtensionSignatureVerificationService, undefined, true)); + services.set(IAllowedExtensionsService, new SyncDescriptor(AllowedExtensionsService, undefined, true)); services.set(INativeServerExtensionManagementService, new SyncDescriptor(ExtensionManagementService, undefined, true)); services.set(IExtensionGalleryService, new SyncDescriptor(ExtensionGalleryServiceWithNoStorageService, undefined, true)); diff --git a/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts b/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts index 6dc6b35c69c93..ee899383305c7 100644 --- a/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts +++ b/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts @@ -21,7 +21,8 @@ import { EXTENSION_INSTALL_SOURCE_CONTEXT, DidUpdateExtensionMetadata, UninstallExtensionInfo, - ExtensionSignatureVerificationCode + ExtensionSignatureVerificationCode, + IAllowedExtensionsService } from './extensionManagement.js'; import { areSameExtensions, ExtensionKey, getGalleryExtensionId, getGalleryExtensionTelemetryData, getLocalExtensionTelemetryData } from './extensionManagementUtil.js'; import { ExtensionType, IExtensionManifest, isApplicationScopedExtension, TargetPlatform } from '../../extensions/common/extensions.js'; @@ -57,7 +58,66 @@ export interface IUninstallExtensionTask { cancel(): void; } -export abstract class AbstractExtensionManagementService extends Disposable implements IExtensionManagementService { +export abstract class CommontExtensionManagementService extends Disposable implements IExtensionManagementService { + + _serviceBrand: undefined; + + constructor( + @IProductService protected readonly productService: IProductService, + @IAllowedExtensionsService protected readonly allowedExtensionsService: IAllowedExtensionsService, + ) { + super(); + } + + async canInstall(extension: IGalleryExtension): Promise { + const allowedToInstall = this.allowedExtensionsService.isAllowed({ id: extension.identifier.id }); + if (allowedToInstall !== true) { + return new MarkdownString(nls.localize('not allowed to install', "This extension cannot be installed because {0}", allowedToInstall.value)); + } + + if (!(await this.isExtensionPlatformCompatible(extension))) { + const productName = isWeb ? nls.localize('VS Code for Web', "{0} for the Web", this.productService.nameLong) : this.productService.nameLong; + const learnLink = isWeb ? 'https://aka.ms/vscode-web-extensions-guide' : 'https://aka.ms/vscode-platform-specific-extensions'; + return new MarkdownString(`${nls.localize('incompatible platform', "The '{0}' extension is not available in {1} for {2}.", + extension.displayName ?? extension.identifier.id, productName, TargetPlatformToString(await this.getTargetPlatform()))} [${nls.localize('learn why', "Learn Why")}](${learnLink})`); + } + + return true; + } + + protected async isExtensionPlatformCompatible(extension: IGalleryExtension): Promise { + const currentTargetPlatform = await this.getTargetPlatform(); + return extension.allTargetPlatforms.some(targetPlatform => isTargetPlatformCompatible(targetPlatform, extension.allTargetPlatforms, currentTargetPlatform)); + } + + abstract readonly onInstallExtension: Event; + abstract readonly onDidInstallExtensions: Event; + abstract readonly onUninstallExtension: Event; + abstract readonly onDidUninstallExtension: Event; + abstract readonly onDidUpdateExtensionMetadata: Event; + abstract installFromGallery(extension: IGalleryExtension, options?: InstallOptions): Promise; + abstract installGalleryExtensions(extensions: InstallExtensionInfo[]): Promise; + abstract uninstall(extension: ILocalExtension, options?: UninstallOptions): Promise; + abstract uninstallExtensions(extensions: UninstallExtensionInfo[]): Promise; + abstract toggleAppliationScope(extension: ILocalExtension, fromProfileLocation: URI): Promise; + abstract getExtensionsControlManifest(): Promise; + abstract resetPinnedStateForAllUserExtensions(pinned: boolean): Promise; + abstract registerParticipant(pariticipant: IExtensionManagementParticipant): void; + abstract getTargetPlatform(): Promise; + abstract zip(extension: ILocalExtension): Promise; + abstract getManifest(vsix: URI): Promise; + abstract install(vsix: URI, options?: InstallOptions): Promise; + abstract installFromLocation(location: URI, profileLocation: URI): Promise; + abstract installExtensionsFromProfile(extensions: IExtensionIdentifier[], fromProfileLocation: URI, toProfileLocation: URI): Promise; + abstract getInstalled(type?: ExtensionType, profileLocation?: URI, productVersion?: IProductVersion): Promise; + abstract copyExtensions(fromProfileLocation: URI, toProfileLocation: URI): Promise; + abstract download(extension: IGalleryExtension, operation: InstallOperation, donotVerifySignature: boolean): Promise; + abstract reinstallFromGallery(extension: ILocalExtension): Promise; + abstract cleanUp(): Promise; + abstract updateMetadata(local: ILocalExtension, metadata: Partial, profileLocation: URI): Promise; +} + +export abstract class AbstractExtensionManagementService extends CommontExtensionManagementService implements IExtensionManagementService { declare readonly _serviceBrand: undefined; @@ -88,10 +148,11 @@ export abstract class AbstractExtensionManagementService extends Disposable impl @ITelemetryService protected readonly telemetryService: ITelemetryService, @IUriIdentityService protected readonly uriIdentityService: IUriIdentityService, @ILogService protected readonly logService: ILogService, - @IProductService protected readonly productService: IProductService, + @IProductService productService: IProductService, + @IAllowedExtensionsService allowedExtensionsService: IAllowedExtensionsService, @IUserDataProfilesService protected readonly userDataProfilesService: IUserDataProfilesService, ) { - super(); + super(productService, allowedExtensionsService); this._register(toDisposable(() => { this.installingExtensions.forEach(({ task }) => task.cancel()); this.uninstallingExtensions.forEach(promise => promise.cancel()); @@ -100,14 +161,6 @@ export abstract class AbstractExtensionManagementService extends Disposable impl })); } - async canInstall(extension: IGalleryExtension): Promise { - const currentTargetPlatform = await this.getTargetPlatform(); - if (extension.allTargetPlatforms.some(targetPlatform => isTargetPlatformCompatible(targetPlatform, extension.allTargetPlatforms, currentTargetPlatform))) { - return true; - } - return new MarkdownString(`${nls.localize('incompatible platform', "The '{0}' extension is not available in {1} for {2}.", extension.displayName ?? extension.identifier.id, this.productService.nameLong, TargetPlatformToString(currentTargetPlatform))} [${nls.localize('learn more', "Learn More")}](https://aka.ms/vscode-platform-specific-extensions)`); - } - async installFromGallery(extension: IGalleryExtension, options: InstallOptions = {}): Promise { try { const results = await this.installGalleryExtensions([{ extension, options }]); @@ -844,20 +897,6 @@ export abstract class AbstractExtensionManagementService extends Disposable impl } } - abstract getTargetPlatform(): Promise; - abstract zip(extension: ILocalExtension): Promise; - abstract getManifest(vsix: URI): Promise; - abstract install(vsix: URI, options?: InstallOptions): Promise; - abstract installFromLocation(location: URI, profileLocation: URI): Promise; - abstract installExtensionsFromProfile(extensions: IExtensionIdentifier[], fromProfileLocation: URI, toProfileLocation: URI): Promise; - abstract getInstalled(type?: ExtensionType, profileLocation?: URI, productVersion?: IProductVersion): Promise; - abstract copyExtensions(fromProfileLocation: URI, toProfileLocation: URI): Promise; - abstract download(extension: IGalleryExtension, operation: InstallOperation, donotVerifySignature: boolean): Promise; - abstract reinstallFromGallery(extension: ILocalExtension): Promise; - abstract cleanUp(): Promise; - - abstract updateMetadata(local: ILocalExtension, metadata: Partial, profileLocation: URI): Promise; - protected abstract getCurrentExtensionsManifestLocation(): URI; protected abstract createInstallExtensionTask(manifest: IExtensionManifest, extension: URI | IGalleryExtension, options: InstallExtensionTaskOptions): IInstallExtensionTask; protected abstract createUninstallExtensionTask(extension: ILocalExtension, options: UninstallExtensionTaskOptions): IUninstallExtensionTask; diff --git a/src/vs/platform/extensionManagement/common/allowedExtensionsService.ts b/src/vs/platform/extensionManagement/common/allowedExtensionsService.ts new file mode 100644 index 0000000000000..dfd7b279c35cc --- /dev/null +++ b/src/vs/platform/extensionManagement/common/allowedExtensionsService.ts @@ -0,0 +1,136 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable } from '../../../base/common/lifecycle.js'; +import { URI } from '../../../base/common/uri.js'; +import * as nls from '../../../nls.js'; +import { IGalleryExtension, AllowedExtensionsConfigKey, IAllowedExtensionsService } from './extensionManagement.js'; +import { isGalleryExtension, isIExtension } from './extensionManagementUtil.js'; +import { IExtension, TargetPlatform } from '../../extensions/common/extensions.js'; +import { IProductService } from '../../product/common/productService.js'; +import { IMarkdownString, MarkdownString } from '../../../base/common/htmlContent.js'; +import { IConfigurationService } from '../../configuration/common/configuration.js'; +import { IStringDictionary } from '../../../base/common/collections.js'; +import { isBoolean, isObject, isUndefined } from '../../../base/common/types.js'; +import { Emitter } from '../../../base/common/event.js'; + +const VersionRegex = /^(?\d+\.\d+\.\d+(-.*)?)(@(?.+))?$/; + +type AllowedExtensionsConfigValueType = IStringDictionary; + +export class AllowedExtensionsService extends Disposable implements IAllowedExtensionsService { + + _serviceBrand: undefined; + + private allowedExtensions: AllowedExtensionsConfigValueType | undefined; + private readonly publisherMappings: IStringDictionary = {}; + + private _onDidChangeAllowedExtensions = this._register(new Emitter()); + readonly onDidChangeAllowedExtensions = this._onDidChangeAllowedExtensions.event; + + constructor( + @IProductService productService: IProductService, + @IConfigurationService protected readonly configurationService: IConfigurationService + ) { + super(); + for (const key in productService.extensionPublisherMappings) { + this.publisherMappings[key.toLowerCase()] = productService.extensionPublisherMappings[key].toLowerCase(); + } + this.allowedExtensions = this.getAllowedExtensionsValue(); + this._register(this.configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(AllowedExtensionsConfigKey)) { + this.allowedExtensions = this.getAllowedExtensionsValue(); + this._onDidChangeAllowedExtensions.fire(); + } + })); + } + + private getAllowedExtensionsValue(): AllowedExtensionsConfigValueType | undefined { + const value = this.configurationService.getValue(AllowedExtensionsConfigKey); + if (!isObject(value) || Array.isArray(value)) { + return undefined; + } + const entries = Object.entries(value).map(([key, value]) => [key.toLowerCase(), value]); + if (entries.length === 1 && entries[0][0] === '*' && entries[0][1] === true) { + return undefined; + } + return Object.fromEntries(entries); + } + + isAllowed(extension: IGalleryExtension | IExtension | { id: string; version?: string; prerelease?: boolean; targetPlatform?: TargetPlatform }): true | IMarkdownString { + if (!this.allowedExtensions) { + return true; + } + + let id: string, version: string, targetPlatform: TargetPlatform, prerelease: boolean, publisher: string; + + if (isGalleryExtension(extension)) { + id = extension.identifier.id.toLowerCase(); + version = extension.version; + prerelease = extension.properties.isPreReleaseVersion; + publisher = extension.publisher.toLowerCase(); + targetPlatform = extension.properties.targetPlatform; + } else if (isIExtension(extension)) { + id = extension.identifier.id.toLowerCase(); + version = extension.manifest.version; + prerelease = extension.preRelease; + publisher = extension.manifest.publisher.toLowerCase(); + targetPlatform = extension.targetPlatform; + } else { + id = extension.id.toLowerCase(); + version = extension.version ?? '*'; + targetPlatform = extension.targetPlatform ?? TargetPlatform.UNIVERSAL; + prerelease = extension.prerelease ?? false; + publisher = extension.id.substring(0, extension.id.indexOf('.')).toLowerCase(); + } + + const settingsCommandLink = URI.parse(`command:workbench.action.openSettings?${encodeURIComponent(JSON.stringify({ query: `@id:${AllowedExtensionsConfigKey}` }))}`).toString(); + const extensionValue = this.allowedExtensions[id]; + const extensionReason = new MarkdownString(nls.localize('specific extension not allowed', "it is not in the [allowed list]({0})", settingsCommandLink)); + if (!isUndefined(extensionValue)) { + if (isBoolean(extensionValue)) { + return extensionValue ? true : extensionReason; + } + if (extensionValue === 'release' && prerelease) { + return new MarkdownString(nls.localize('extension prerelease not allowed', "the pre-release versions of this extension are not in the [allowed list]({0})", settingsCommandLink)); + } + if (version !== '*' && Array.isArray(extensionValue) && !extensionValue.some(v => { + const match = VersionRegex.exec(v); + if (match && match.groups) { + const { platform: p, version: v } = match.groups; + if (v !== version) { + return false; + } + if (targetPlatform !== TargetPlatform.UNIVERSAL && p && targetPlatform !== p) { + return false; + } + return true; + } + return false; + })) { + return extensionReason; + } + return true; + } + + publisher = (this.publisherMappings[publisher])?.toLowerCase() ?? publisher; + const publisherValue = this.allowedExtensions[publisher]; + if (!isUndefined(publisherValue)) { + if (isBoolean(publisherValue)) { + return publisherValue ? true : new MarkdownString(nls.localize('publisher not allowed', "the extensions from this publisher are not in the [allowed list]({1})", publisher, settingsCommandLink)); + } + if (publisherValue === 'release' && prerelease) { + return new MarkdownString(nls.localize('prerelease versions from this publisher not allowed', "the pre-release versions from this publisher are not in the [allowed list]({1})", publisher, settingsCommandLink)); + } + return true; + } + + if (this.allowedExtensions['*'] === true) { + return true; + } + + return extensionReason; + } +} diff --git a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts index 28282587c55cd..35da874bec37c 100644 --- a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts +++ b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts @@ -15,7 +15,7 @@ import { URI } from '../../../base/common/uri.js'; import { IHeaders, IRequestContext, IRequestOptions, isOfflineError } from '../../../base/parts/request/common/request.js'; import { IConfigurationService } from '../../configuration/common/configuration.js'; import { IEnvironmentService } from '../../environment/common/environment.js'; -import { getTargetPlatform, IExtensionGalleryService, IExtensionIdentifier, IExtensionInfo, IGalleryExtension, IGalleryExtensionAsset, IGalleryExtensionAssets, IGalleryExtensionVersion, InstallOperation, IQueryOptions, IExtensionsControlManifest, isNotWebExtensionInWebTargetPlatform, isTargetPlatformCompatible, ITranslation, SortBy, SortOrder, StatisticType, toTargetPlatform, WEB_EXTENSION_TAG, IExtensionQueryOptions, IDeprecationInfo, ISearchPrefferedResults, ExtensionGalleryError, ExtensionGalleryErrorCode, IProductVersion, UseUnpkgResourceApi } from './extensionManagement.js'; +import { getTargetPlatform, IExtensionGalleryService, IExtensionIdentifier, IExtensionInfo, IGalleryExtension, IGalleryExtensionAsset, IGalleryExtensionAssets, IGalleryExtensionVersion, InstallOperation, IQueryOptions, IExtensionsControlManifest, isNotWebExtensionInWebTargetPlatform, isTargetPlatformCompatible, ITranslation, SortBy, SortOrder, StatisticType, toTargetPlatform, WEB_EXTENSION_TAG, IExtensionQueryOptions, IDeprecationInfo, ISearchPrefferedResults, ExtensionGalleryError, ExtensionGalleryErrorCode, IProductVersion, UseUnpkgResourceApiConfigKey, IAllowedExtensionsService } from './extensionManagement.js'; import { adoptToGalleryExtensionId, areSameExtensions, getGalleryExtensionId, getGalleryExtensionTelemetryData } from './extensionManagementUtil.js'; import { IExtensionManifest, TargetPlatform } from '../../extensions/common/extensions.js'; import { areApiProposalsCompatible, isEngineValid } from '../../extensions/common/extensionValidator.js'; @@ -619,6 +619,7 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi @IFileService private readonly fileService: IFileService, @IProductService private readonly productService: IProductService, @IConfigurationService private readonly configurationService: IConfigurationService, + @IAllowedExtensionsService private readonly allowedExtensionsService: IAllowedExtensionsService, ) { const config = productService.extensionsGallery; const isPPEEnabled = config?.servicePPEUrl && configurationService.getValue('_extensionsGallery.enablePPE'); @@ -655,7 +656,7 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi const options = CancellationToken.isCancellationToken(arg1) ? {} : arg1 as IExtensionQueryOptions; const token = CancellationToken.isCancellationToken(arg1) ? arg1 : arg2 as CancellationToken; - const useResourceApi = options.preferResourceApi && (this.configurationService.getValue(UseUnpkgResourceApi) ?? false); + const useResourceApi = options.preferResourceApi && (this.configurationService.getValue(UseUnpkgResourceApiConfigKey) ?? false); const result = useResourceApi ? await this.getExtensionsUsingResourceApi(extensionInfos, options, token) : await this.doGetExtensions(extensionInfos, options, token); @@ -764,7 +765,6 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi // report telemetry else { - toQuery.push(extensionInfo); this.telemetryService.publicLog2< { extension: string; @@ -782,6 +782,9 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi preRelease: !!extensionInfo.preRelease, compatible: !!options.compatible }); + if (!options.compatible || this.allowedExtensionsService.isAllowed({ id: extensionInfo.id }) === true) { + toQuery.push(extensionInfo); + } } } catch (error) { // Skip if there is an error while getting the latest version @@ -803,6 +806,9 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi if (await this.isExtensionCompatible(extension, includePreRelease, targetPlatform)) { return extension; } + if (this.allowedExtensionsService.isAllowed({ id: extension.identifier.id }) !== true) { + return null; + } const query = new Query() .withFlags(Flags.IncludeVersions) .withPage(1, 1) @@ -812,6 +818,10 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi } async isExtensionCompatible(extension: IGalleryExtension, includePreRelease: boolean, targetPlatform: TargetPlatform, productVersion: IProductVersion = { version: this.productService.version, date: this.productService.date }): Promise { + if (this.allowedExtensionsService.isAllowed(extension) !== true) { + return false; + } + if (!isTargetPlatformCompatible(extension.properties.targetPlatform, extension.allTargetPlatforms, targetPlatform)) { return false; } @@ -852,7 +862,8 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi } private async isValidVersion(extension: string, rawGalleryExtensionVersion: IRawGalleryExtensionVersion, versionType: 'release' | 'prerelease' | 'any', compatible: boolean, allTargetPlatforms: TargetPlatform[], targetPlatform: TargetPlatform, productVersion: IProductVersion = { version: this.productService.version, date: this.productService.date }): Promise { - if (!isTargetPlatformCompatible(getTargetPlatformForExtensionVersion(rawGalleryExtensionVersion), allTargetPlatforms, targetPlatform)) { + const targetPlatformForExtension = getTargetPlatformForExtensionVersion(rawGalleryExtensionVersion); + if (!isTargetPlatformCompatible(targetPlatformForExtension, allTargetPlatforms, targetPlatform)) { return false; } @@ -861,6 +872,9 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi } if (compatible) { + if (this.allowedExtensionsService.isAllowed({ id: extension, version: rawGalleryExtensionVersion.version, prerelease: versionType === 'any' ? undefined : isPreReleaseVersion(rawGalleryExtensionVersion), targetPlatform: targetPlatformForExtension }) !== true) { + return false; + } try { const engine = await this.getEngine(extension, rawGalleryExtensionVersion); if (!isEngineValid(engine, productVersion.version, productVersion.date)) { @@ -990,11 +1004,20 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi const rawGalleryExtension = rawGalleryExtensions[index]; const extensionIdentifier = { id: getGalleryExtensionId(rawGalleryExtension.publisher.publisherName, rawGalleryExtension.extensionName), uuid: rawGalleryExtension.extensionId }; const includePreRelease = isBoolean(criteria.includePreRelease) ? criteria.includePreRelease : !!criteria.includePreRelease.find(extensionIdentifierWithPreRelease => areSameExtensions(extensionIdentifierWithPreRelease, extensionIdentifier))?.includePreRelease; - if (criteria.compatible && isNotWebExtensionInWebTargetPlatform(getAllTargetPlatforms(rawGalleryExtension), criteria.targetPlatform)) { + if (criteria.compatible) { /** Skip if requested for a web-compatible extension and it is not a web extension. * All versions are not needed in this case */ - continue; + if (isNotWebExtensionInWebTargetPlatform(getAllTargetPlatforms(rawGalleryExtension), criteria.targetPlatform)) { + continue; + } + /** + * Skip if the extension is not allowed. + * All versions are not needed in this case + */ + if (this.allowedExtensionsService.isAllowed({ id: extensionIdentifier.id }) !== true) { + continue; + } } const extension = await this.toGalleryExtensionWithCriteria(rawGalleryExtension, criteria, context); if (!extension @@ -1553,8 +1576,9 @@ export class ExtensionGalleryService extends AbstractExtensionGalleryService { @IFileService fileService: IFileService, @IProductService productService: IProductService, @IConfigurationService configurationService: IConfigurationService, + @IAllowedExtensionsService allowedExtensionsService: IAllowedExtensionsService, ) { - super(storageService, requestService, logService, environmentService, telemetryService, fileService, productService, configurationService); + super(storageService, requestService, logService, environmentService, telemetryService, fileService, productService, configurationService, allowedExtensionsService); } } @@ -1568,7 +1592,8 @@ export class ExtensionGalleryServiceWithNoStorageService extends AbstractExtensi @IFileService fileService: IFileService, @IProductService productService: IProductService, @IConfigurationService configurationService: IConfigurationService, + @IAllowedExtensionsService allowedExtensionsService: IAllowedExtensionsService, ) { - super(undefined, requestService, logService, environmentService, telemetryService, fileService, productService, configurationService); + super(undefined, requestService, logService, environmentService, telemetryService, fileService, productService, configurationService, allowedExtensionsService); } } diff --git a/src/vs/platform/extensionManagement/common/extensionManagement.ts b/src/vs/platform/extensionManagement/common/extensionManagement.ts index f6bf93a0b26a9..e810cd5171119 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagement.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagement.ts @@ -628,6 +628,14 @@ export interface IExtensionTipsService { getOtherExecutableBasedTips(): Promise; } +export const IAllowedExtensionsService = createDecorator('IAllowedExtensionsService'); +export interface IAllowedExtensionsService { + readonly _serviceBrand: undefined; + + readonly onDidChangeAllowedExtensions: Event; + isAllowed(extension: IGalleryExtension | IExtension | { id: string; version?: string; prerelease?: boolean }): true | IMarkdownString; +} + export async function computeSize(location: URI, fileService: IFileService): Promise { const stat = await fileService.resolve(location); if (stat.children) { @@ -639,4 +647,5 @@ export async function computeSize(location: URI, fileService: IFileService): Pro export const ExtensionsLocalizedLabel = localize2('extensions', "Extensions"); export const PreferencesLocalizedLabel = localize2('preferences', 'Preferences'); -export const UseUnpkgResourceApi = 'extensions.gallery.useUnpkgResourceApi'; +export const UseUnpkgResourceApiConfigKey = 'extensions.gallery.useUnpkgResourceApi'; +export const AllowedExtensionsConfigKey = 'extensions.allowed'; diff --git a/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts b/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts index 4c1821509ea69..d9542cb5e6a98 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts @@ -4,16 +4,19 @@ *--------------------------------------------------------------------------------------------*/ import { Emitter, Event } from '../../../base/common/event.js'; -import { Disposable } from '../../../base/common/lifecycle.js'; import { cloneAndChange } from '../../../base/common/objects.js'; import { URI, UriComponents } from '../../../base/common/uri.js'; import { DefaultURITransformer, IURITransformer, transformAndReviveIncomingURIs } from '../../../base/common/uriIpc.js'; import { IChannel, IServerChannel } from '../../../base/parts/ipc/common/ipc.js'; -import { IExtensionIdentifier, IExtensionTipsService, IGalleryExtension, ILocalExtension, IExtensionsControlManifest, isTargetPlatformCompatible, InstallOptions, UninstallOptions, Metadata, IExtensionManagementService, DidUninstallExtensionEvent, InstallExtensionEvent, InstallExtensionResult, UninstallExtensionEvent, InstallOperation, InstallExtensionInfo, IProductVersion, DidUpdateExtensionMetadata, UninstallExtensionInfo, TargetPlatformToString } from './extensionManagement.js'; +import { + IExtensionIdentifier, IExtensionTipsService, IGalleryExtension, ILocalExtension, IExtensionsControlManifest, InstallOptions, + UninstallOptions, Metadata, IExtensionManagementService, DidUninstallExtensionEvent, InstallExtensionEvent, InstallExtensionResult, + UninstallExtensionEvent, InstallOperation, InstallExtensionInfo, IProductVersion, DidUpdateExtensionMetadata, UninstallExtensionInfo, + IAllowedExtensionsService +} from './extensionManagement.js'; import { ExtensionType, IExtensionManifest, TargetPlatform } from '../../extensions/common/extensions.js'; -import { IMarkdownString, MarkdownString } from '../../../base/common/htmlContent.js'; -import { localize } from '../../../nls.js'; import { IProductService } from '../../product/common/productService.js'; +import { CommontExtensionManagementService } from './abstractExtensionManagementService.js'; function transformIncomingURI(uri: UriComponents, transformer: IURITransformer | null): URI; function transformIncomingURI(uri: UriComponents | undefined, transformer: IURITransformer | null): URI | undefined; @@ -183,7 +186,7 @@ export interface ExtensionEventResult { readonly applicationScoped?: boolean; } -export class ExtensionManagementChannelClient extends Disposable implements IExtensionManagementService { +export class ExtensionManagementChannelClient extends CommontExtensionManagementService implements IExtensionManagementService { declare readonly _serviceBrand: undefined; @@ -204,9 +207,10 @@ export class ExtensionManagementChannelClient extends Disposable implements IExt constructor( private readonly channel: IChannel, - protected readonly productService: IProductService + productService: IProductService, + allowedExtensionsService: IAllowedExtensionsService, ) { - super(); + super(productService, allowedExtensionsService); this._register(this.channel.listen('onInstallExtension')(e => this.onInstallExtensionEvent({ ...e, source: this.isUriComponents(e.source) ? URI.revive(e.source) : e.source, profileLocation: URI.revive(e.profileLocation) }))); this._register(this.channel.listen('onDidInstallExtensions')(results => this.onDidInstallExtensionsEvent(results.map(e => ({ ...e, local: e.local ? transformIncomingExtension(e.local, null) : e.local, source: this.isUriComponents(e.source) ? URI.revive(e.source) : e.source, profileLocation: URI.revive(e.profileLocation) }))))); this._register(this.channel.listen('onUninstallExtension')(e => this.onUninstallExtensionEvent({ ...e, profileLocation: URI.revive(e.profileLocation) }))); @@ -250,14 +254,6 @@ export class ExtensionManagementChannelClient extends Disposable implements IExt return this._targetPlatformPromise; } - async canInstall(extension: IGalleryExtension): Promise { - const currentTargetPlatform = await this.getTargetPlatform(); - if (extension.allTargetPlatforms.some(targetPlatform => isTargetPlatformCompatible(targetPlatform, extension.allTargetPlatforms, currentTargetPlatform))) { - return true; - } - return new MarkdownString(`${localize('incompatible platform', "The '{0}' extension is not available in {1} for {2}.", extension.displayName ?? extension.identifier.id, this.productService.nameLong, TargetPlatformToString(currentTargetPlatform))} [${localize('learn more', "Learn More")}](https://aka.ms/vscode-platform-specific-extensions)`); - } - zip(extension: ILocalExtension): Promise { return Promise.resolve(this.channel.call('zip', [extension]).then(result => URI.revive(result))); } diff --git a/src/vs/platform/extensionManagement/common/extensionManagementUtil.ts b/src/vs/platform/extensionManagement/common/extensionManagementUtil.ts index 9b0ef7b6ff419..15399a1179237 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagementUtil.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagementUtil.ts @@ -5,7 +5,7 @@ import { compareIgnoreCase } from '../../../base/common/strings.js'; import { IExtensionIdentifier, IGalleryExtension, ILocalExtension, getTargetPlatform } from './extensionManagement.js'; -import { ExtensionIdentifier, IExtension, TargetPlatform, UNDEFINED_PUBLISHER } from '../../extensions/common/extensions.js'; +import { ExtensionIdentifier, ExtensionType, IExtension, TargetPlatform, UNDEFINED_PUBLISHER } from '../../extensions/common/extensions.js'; import { IFileService } from '../../files/common/files.js'; import { isLinux, platform } from '../../../base/common/platform.js'; import { URI } from '../../../base/common/uri.js'; @@ -196,3 +196,11 @@ export async function computeTargetPlatform(fileService: IFileService, logServic logService.debug('ComputeTargetPlatform:', targetPlatform); return targetPlatform; } + +export function isGalleryExtension(extension: any): extension is IGalleryExtension { + return extension.type === 'gallery'; +} + +export function isIExtension(extension: any): extension is IExtension { + return extension.type === ExtensionType.User || extension.type === ExtensionType.System; +} diff --git a/src/vs/platform/extensionManagement/common/extensionsScannerService.ts b/src/vs/platform/extensionManagement/common/extensionsScannerService.ts index 0b2e581229429..99868cddb5d63 100644 --- a/src/vs/platform/extensionManagement/common/extensionsScannerService.ts +++ b/src/vs/platform/extensionManagement/common/extensionsScannerService.ts @@ -50,6 +50,7 @@ interface IRelaxedScannedExtension { metadata: Metadata | undefined; isValid: boolean; validations: readonly [Severity, string][]; + preRelease: boolean; } export type IScannedExtension = Readonly & { manifest: IExtensionManifest }; @@ -671,7 +672,8 @@ class ExtensionsScanner extends Disposable { publisherDisplayName: metadata?.publisherDisplayName, metadata, isValid: true, - validations: [] + validations: [], + preRelease: !!metadata?.preRelease, }; if (input.validate) { extension = this.validate(extension, input); @@ -1000,6 +1002,7 @@ export function toExtensionDescription(extension: IScannedExtension, isUnderDeve uuid: extension.identifier.uuid, targetPlatform: extension.targetPlatform, publisherDisplayName: extension.publisherDisplayName, + preRelease: extension.preRelease, ...extension.manifest, }; } diff --git a/src/vs/platform/extensionManagement/node/extensionManagementService.ts b/src/vs/platform/extensionManagement/node/extensionManagementService.ts index 585272ac91d1b..e4bcb6c08668c 100644 --- a/src/vs/platform/extensionManagement/node/extensionManagementService.ts +++ b/src/vs/platform/extensionManagement/node/extensionManagementService.ts @@ -34,6 +34,7 @@ import { EXTENSION_INSTALL_CLIENT_TARGET_PLATFORM_CONTEXT, ExtensionSignatureVerificationCode, computeSize, + IAllowedExtensionsService, } from '../common/extensionManagement.js'; import { areSameExtensions, computeTargetPlatform, ExtensionKey, getGalleryExtensionId, groupByExtension } from '../common/extensionManagementUtil.js'; import { IExtensionsProfileScannerService, IScannedProfileExtension } from '../common/extensionsProfileScannerService.js'; @@ -87,10 +88,11 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi @IFileService private readonly fileService: IFileService, @IConfigurationService private readonly configurationService: IConfigurationService, @IProductService productService: IProductService, + @IAllowedExtensionsService allowedExtensionsService: IAllowedExtensionsService, @IUriIdentityService uriIdentityService: IUriIdentityService, @IUserDataProfilesService userDataProfilesService: IUserDataProfilesService ) { - super(galleryService, telemetryService, uriIdentityService, logService, productService, userDataProfilesService); + super(galleryService, telemetryService, uriIdentityService, logService, productService, allowedExtensionsService, userDataProfilesService); const extensionLifecycle = this._register(instantiationService.createInstance(ExtensionsLifecycle)); this.extensionsScanner = this._register(instantiationService.createInstance(ExtensionsScanner, extension => extensionLifecycle.postUninstall(extension))); this.manifestCache = this._register(new ExtensionsManifestCache(userDataProfilesService, fileService, uriIdentityService, this, this.logService)); @@ -896,7 +898,7 @@ export class ExtensionsScanner extends Disposable { isMachineScoped: !!extension.metadata?.isMachineScoped, isPreReleaseVersion: !!extension.metadata?.isPreReleaseVersion, hasPreReleaseVersion: !!extension.metadata?.hasPreReleaseVersion, - preRelease: !!extension.metadata?.preRelease, + preRelease: extension.preRelease, installedTimestamp: extension.metadata?.installedTimestamp, updated: !!extension.metadata?.updated, pinned: !!extension.metadata?.pinned, diff --git a/src/vs/platform/extensionManagement/test/common/allowedExtensionsService.test.ts b/src/vs/platform/extensionManagement/test/common/allowedExtensionsService.test.ts new file mode 100644 index 0000000000000..a9c68485dfbcf --- /dev/null +++ b/src/vs/platform/extensionManagement/test/common/allowedExtensionsService.test.ts @@ -0,0 +1,233 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { AllowedExtensionsService } from '../../common/allowedExtensionsService.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; +import { IProductService } from '../../../product/common/productService.js'; +import { TestConfigurationService } from '../../../configuration/test/common/testConfigurationService.js'; +import { IStringDictionary } from '../../../../base/common/collections.js'; +import { AllowedExtensionsConfigKey, IGalleryExtension, ILocalExtension } from '../../common/extensionManagement.js'; +import { ExtensionType, IExtensionManifest, TargetPlatform } from '../../../extensions/common/extensions.js'; +import { Event } from '../../../../base/common/event.js'; +import { ConfigurationTarget } from '../../../configuration/common/configuration.js'; +import { getGalleryExtensionId } from '../../common/extensionManagementUtil.js'; +import { generateUuid } from '../../../../base/common/uuid.js'; +import { URI } from '../../../../base/common/uri.js'; + +suite('AllowedExtensionsService', () => { + + const disposables = ensureNoDisposablesAreLeakedInTestSuite(); + + const configurationService = new TestConfigurationService(); + + setup(() => { + configurationService.setUserConfiguration(AllowedExtensionsConfigKey, '*'); + }); + + test('should allow all extensions if no allowed extensions are configured', () => { + const testObject = disposables.add(new AllowedExtensionsService(aProductService(), configurationService)); + assert.strictEqual(testObject.isAllowed({ id: 'test.extension' }) === true, true); + }); + + test('should not allow specific extension if not in allowed list', () => { + configurationService.setUserConfiguration(AllowedExtensionsConfigKey, { 'test.extension': false }); + const testObject = disposables.add(new AllowedExtensionsService(aProductService(), configurationService)); + assert.strictEqual(testObject.isAllowed({ id: 'test.extension' }) === true, false); + }); + + test('should allow specific extension if in allowed list', () => { + configurationService.setUserConfiguration(AllowedExtensionsConfigKey, { 'test.extension': true }); + const testObject = disposables.add(new AllowedExtensionsService(aProductService(), configurationService)); + assert.strictEqual(testObject.isAllowed({ id: 'test.extension' }) === true, true); + }); + + test('should not allow pre-release extension if only release is allowed', () => { + configurationService.setUserConfiguration(AllowedExtensionsConfigKey, { 'test.extension': 'release' }); + const testObject = disposables.add(new AllowedExtensionsService(aProductService(), configurationService)); + assert.strictEqual(testObject.isAllowed({ id: 'test.extension', prerelease: true }) === true, false); + }); + + test('should allow pre-release extension if pre-release is allowed', () => { + configurationService.setUserConfiguration(AllowedExtensionsConfigKey, { 'test.extension': true }); + const testObject = disposables.add(new AllowedExtensionsService(aProductService(), configurationService)); + assert.strictEqual(testObject.isAllowed({ id: 'test.extension', prerelease: true }) === true, true); + }); + + test('should allow specific version of an extension when configured to that version', () => { + configurationService.setUserConfiguration(AllowedExtensionsConfigKey, { 'test.extension': ['1.2.3'] }); + const testObject = disposables.add(new AllowedExtensionsService(aProductService(), configurationService)); + assert.strictEqual(testObject.isAllowed({ id: 'test.extension', version: '1.2.3' }) === true, true); + }); + + test('should allow any version of an extension when a specific version is configured', () => { + configurationService.setUserConfiguration(AllowedExtensionsConfigKey, { 'test.extension': ['1.2.3'] }); + const testObject = disposables.add(new AllowedExtensionsService(aProductService(), configurationService)); + assert.strictEqual(testObject.isAllowed({ id: 'test.extension' }) === true, true); + }); + + test('should allow any version of an extension when release is configured', () => { + configurationService.setUserConfiguration(AllowedExtensionsConfigKey, { 'test.extension': 'release' }); + const testObject = disposables.add(new AllowedExtensionsService(aProductService(), configurationService)); + assert.strictEqual(testObject.isAllowed({ id: 'test.extension' }) === true, true); + }); + + test('should allow a version of an extension when release is configured', () => { + configurationService.setUserConfiguration(AllowedExtensionsConfigKey, { 'test.extension': 'release' }); + const testObject = disposables.add(new AllowedExtensionsService(aProductService(), configurationService)); + assert.strictEqual(testObject.isAllowed({ id: 'test.extension', version: '1.2.3' }) === true, true); + }); + + test('should allow a pre-release version of an extension when release is configured', () => { + configurationService.setUserConfiguration(AllowedExtensionsConfigKey, { 'test.extension': 'release' }); + const testObject = disposables.add(new AllowedExtensionsService(aProductService(), configurationService)); + assert.strictEqual(testObject.isAllowed({ id: 'test.extension', version: '1.2.3', prerelease: true }) === true, false); + }); + + test('should allow specific version of an extension when configured to multiple versions', () => { + configurationService.setUserConfiguration(AllowedExtensionsConfigKey, { 'test.extension': ['1.2.3', '2.0.1', '3.1.2'] }); + const testObject = disposables.add(new AllowedExtensionsService(aProductService(), configurationService)); + assert.strictEqual(testObject.isAllowed({ id: 'test.extension', version: '1.2.3' }) === true, true); + }); + + test('should allow platform specific version of an extension when configured to platform specific version', () => { + configurationService.setUserConfiguration(AllowedExtensionsConfigKey, { 'test.extension': ['1.2.3@darwin-x64'] }); + const testObject = disposables.add(new AllowedExtensionsService(aProductService(), configurationService)); + assert.strictEqual(testObject.isAllowed({ id: 'test.extension', version: '1.2.3', targetPlatform: TargetPlatform.DARWIN_X64 }) === true, true); + }); + + test('should allow universal platform specific version of an extension when configured to platform specific version', () => { + configurationService.setUserConfiguration(AllowedExtensionsConfigKey, { 'test.extension': ['1.2.3@darwin-x64'] }); + const testObject = disposables.add(new AllowedExtensionsService(aProductService(), configurationService)); + assert.strictEqual(testObject.isAllowed({ id: 'test.extension', version: '1.2.3', targetPlatform: TargetPlatform.UNIVERSAL }) === true, true); + }); + + test('should allow specific version of an extension when configured to platform specific version', () => { + configurationService.setUserConfiguration(AllowedExtensionsConfigKey, { 'test.extension': ['1.2.3@darwin-x64'] }); + const testObject = disposables.add(new AllowedExtensionsService(aProductService(), configurationService)); + assert.strictEqual(testObject.isAllowed({ id: 'test.extension', version: '1.2.3' }) === true, true); + }); + + test('should allow platform specific version of an extension when configured to multiple versions', () => { + configurationService.setUserConfiguration(AllowedExtensionsConfigKey, { 'test.extension': ['1.0.0', '1.2.3@darwin-x64', '1.2.3@darwin-arm64'] }); + const testObject = disposables.add(new AllowedExtensionsService(aProductService(), configurationService)); + assert.strictEqual(testObject.isAllowed({ id: 'test.extension', version: '1.2.3', targetPlatform: TargetPlatform.DARWIN_X64 }) === true, true); + }); + + test('should not allow platform specific version of an extension when configured to different platform specific version', () => { + configurationService.setUserConfiguration(AllowedExtensionsConfigKey, { 'test.extension': ['1.2.3@darwin-x64'] }); + const testObject = disposables.add(new AllowedExtensionsService(aProductService(), configurationService)); + assert.strictEqual(testObject.isAllowed({ id: 'test.extension', version: '1.2.3', targetPlatform: TargetPlatform.DARWIN_ARM64 }) === true, false); + }); + + test('should specific version of an extension when configured to different versions', () => { + configurationService.setUserConfiguration(AllowedExtensionsConfigKey, { 'test.extension': ['1.0.0', '1.2.3@darwin-x64', '1.2.3@darwin-arm64'] }); + const testObject = disposables.add(new AllowedExtensionsService(aProductService(), configurationService)); + assert.strictEqual(testObject.isAllowed({ id: 'test.extension', version: '1.0.1' }) === true, false); + }); + + test('should allow extension if publisher is in allowed list', () => { + configurationService.setUserConfiguration(AllowedExtensionsConfigKey, { 'test': true }); + const testObject = disposables.add(new AllowedExtensionsService(aProductService(), configurationService)); + assert.strictEqual(testObject.isAllowed({ id: 'test.extension' }), true); + }); + + test('should allow extension if publisher is not in allowed list and has publisher mapping', () => { + configurationService.setUserConfiguration(AllowedExtensionsConfigKey, { 'hello': true }); + const testObject = disposables.add(new AllowedExtensionsService(aProductService({ 'test': 'hello' }), configurationService)); + assert.strictEqual(testObject.isAllowed({ id: 'test.extension' }), true); + }); + + test('should allow extension if publisher is not in allowed list and has different publisher mapping', () => { + configurationService.setUserConfiguration(AllowedExtensionsConfigKey, { 'hello': true }); + const testObject = disposables.add(new AllowedExtensionsService(aProductService({ 'test': 'bar' }), configurationService)); + assert.strictEqual(testObject.isAllowed({ id: 'test.extension' }) === true, false); + }); + + test('should not allow extension if publisher is not in allowed list', () => { + configurationService.setUserConfiguration(AllowedExtensionsConfigKey, { 'test': false }); + const testObject = disposables.add(new AllowedExtensionsService(aProductService(), configurationService)); + assert.strictEqual(testObject.isAllowed({ id: 'test.extension' }) === true, false); + }); + + test('should not allow prerelease extension if publisher is allowed only to release', () => { + configurationService.setUserConfiguration(AllowedExtensionsConfigKey, { 'test': 'release' }); + const testObject = disposables.add(new AllowedExtensionsService(aProductService(), configurationService)); + assert.strictEqual(testObject.isAllowed({ id: 'test.extension', prerelease: true }) === true, false); + }); + + test('should allow extension if publisher is set to random value', () => { + configurationService.setUserConfiguration(AllowedExtensionsConfigKey, { 'test': 'hello' }); + const testObject = disposables.add(new AllowedExtensionsService(aProductService(), configurationService)); + assert.strictEqual(testObject.isAllowed({ id: 'test.extension', prerelease: true }) === true, true); + }); + + test('should allow extension if only wildcard is in allowed list', () => { + configurationService.setUserConfiguration(AllowedExtensionsConfigKey, { '*': true }); + const testObject = disposables.add(new AllowedExtensionsService(aProductService(), configurationService)); + assert.strictEqual(testObject.isAllowed({ id: 'test.extension' }), true); + }); + + test('should allow extension if wildcard is in allowed list', () => { + configurationService.setUserConfiguration(AllowedExtensionsConfigKey, { '*': true, 'hello': false }); + const testObject = disposables.add(new AllowedExtensionsService(aProductService(), configurationService)); + assert.strictEqual(testObject.isAllowed({ id: 'test.extension' }), true); + }); + + test('should not allow extension if wildcard is not in allowed list', () => { + configurationService.setUserConfiguration(AllowedExtensionsConfigKey, { '*': false, 'hello': true }); + const testObject = disposables.add(new AllowedExtensionsService(aProductService(), configurationService)); + assert.strictEqual(testObject.isAllowed({ id: 'test.extension' }) === true, false); + }); + + test('should allow a gallery extension', () => { + configurationService.setUserConfiguration(AllowedExtensionsConfigKey, { 'pub': true }); + const testObject = disposables.add(new AllowedExtensionsService(aProductService(), configurationService)); + assert.strictEqual(testObject.isAllowed(aGalleryExtension('name')) === true, true); + }); + + test('should allow a local extension', () => { + configurationService.setUserConfiguration(AllowedExtensionsConfigKey, { 'pub': true }); + const testObject = disposables.add(new AllowedExtensionsService(aProductService(), configurationService)); + assert.strictEqual(testObject.isAllowed(aLocalExtension('pub.name')) === true, true); + }); + + test('should trigger change event when allowed list change', async () => { + configurationService.setUserConfiguration(AllowedExtensionsConfigKey, { '*': false }); + const testObject = disposables.add(new AllowedExtensionsService(aProductService(), configurationService)); + const promise = Event.toPromise(testObject.onDidChangeAllowedExtensions); + configurationService.onDidChangeConfigurationEmitter.fire({ affectsConfiguration: () => true, affectedKeys: new Set([AllowedExtensionsConfigKey]), change: { keys: [], overrides: [] }, source: ConfigurationTarget.USER }); + await promise; + }); + + function aProductService(extensionPublisherMappings?: IStringDictionary): IProductService { + return { + _serviceBrand: undefined, + extensionPublisherMappings + } as IProductService; + } + + function aGalleryExtension(name: string, properties: any = {}, galleryExtensionProperties: any = {}): IGalleryExtension { + const galleryExtension = Object.create({ type: 'gallery', name, publisher: 'pub', version: '1.0.0', allTargetPlatforms: [TargetPlatform.UNIVERSAL], properties: {}, assets: {}, isSigned: true, ...properties }); + galleryExtension.properties = { ...galleryExtension.properties, dependencies: [], ...galleryExtensionProperties }; + galleryExtension.identifier = { id: getGalleryExtensionId(galleryExtension.publisher, galleryExtension.name), uuid: generateUuid() }; + return galleryExtension; + } + + function aLocalExtension(id: string, manifest: Partial = {}, properties: any = {}): ILocalExtension { + const [publisher, name] = id.split('.'); + manifest = { name, publisher, ...manifest }; + properties = { + identifier: { id }, + location: URI.file(`pub.${name}`), + galleryIdentifier: { id, uuid: undefined }, + type: ExtensionType.User, + ...properties, + isValid: properties.isValid ?? true, + }; + properties.isBuiltin = properties.type === ExtensionType.System; + return Object.create({ manifest, ...properties }); + } +}); diff --git a/src/vs/platform/extensionManagement/test/common/extensionsProfileScannerService.test.ts b/src/vs/platform/extensionManagement/test/common/extensionsProfileScannerService.test.ts index 6c02f4be335bb..0329b90263b4e 100644 --- a/src/vs/platform/extensionManagement/test/common/extensionsProfileScannerService.test.ts +++ b/src/vs/platform/extensionManagement/test/common/extensionsProfileScannerService.test.ts @@ -582,6 +582,7 @@ suite('ExtensionsProfileScannerService', () => { ...manifest, }, isValid: true, + preRelease: false, validations: [], ...e }; diff --git a/src/vs/platform/extensions/common/extensions.ts b/src/vs/platform/extensions/common/extensions.ts index 5973d4dcf1204..cee0043bad64d 100644 --- a/src/vs/platform/extensions/common/extensions.ts +++ b/src/vs/platform/extensions/common/extensions.ts @@ -318,6 +318,7 @@ export interface IExtension { readonly changelogUrl?: URI; readonly isValid: boolean; readonly validations: readonly [Severity, string][]; + readonly preRelease: boolean; } /** @@ -456,6 +457,7 @@ export interface IRelaxedExtensionDescription extends IRelaxedExtensionManifest isUserBuiltin: boolean; isUnderDevelopment: boolean; extensionLocation: URI; + preRelease: boolean; } export type IExtensionDescription = Readonly; diff --git a/src/vs/server/node/remoteExtensionHostAgentCli.ts b/src/vs/server/node/remoteExtensionHostAgentCli.ts index 7e30e9af68816..cd1fca04f58b1 100644 --- a/src/vs/server/node/remoteExtensionHostAgentCli.ts +++ b/src/vs/server/node/remoteExtensionHostAgentCli.ts @@ -12,7 +12,7 @@ import { IRequestService } from '../../platform/request/common/request.js'; import { RequestService } from '../../platform/request/node/requestService.js'; import { NullTelemetryService } from '../../platform/telemetry/common/telemetryUtils.js'; import { ITelemetryService } from '../../platform/telemetry/common/telemetry.js'; -import { IExtensionGalleryService, InstallOptions } from '../../platform/extensionManagement/common/extensionManagement.js'; +import { IAllowedExtensionsService, IExtensionGalleryService, InstallOptions } from '../../platform/extensionManagement/common/extensionManagement.js'; import { ExtensionGalleryServiceWithNoStorageService } from '../../platform/extensionManagement/common/extensionGalleryService.js'; import { ExtensionManagementService, INativeServerExtensionManagementService } from '../../platform/extensionManagement/node/extensionManagementService.js'; import { ExtensionSignatureVerificationService, IExtensionSignatureVerificationService } from '../../platform/extensionManagement/node/extensionSignatureVerificationService.js'; @@ -50,6 +50,7 @@ import { LogService } from '../../platform/log/common/logService.js'; import { LoggerService } from '../../platform/log/node/loggerService.js'; import { localize } from '../../nls.js'; import { addUNCHostToAllowlist, disableUNCAccessRestrictions } from '../../base/node/unc.js'; +import { AllowedExtensionsService } from '../../platform/extensionManagement/common/allowedExtensionsService.js'; class CliMain extends Disposable { @@ -135,6 +136,7 @@ class CliMain extends Disposable { services.set(IExtensionsProfileScannerService, new SyncDescriptor(ExtensionsProfileScannerService)); services.set(IExtensionsScannerService, new SyncDescriptor(ExtensionsScannerService)); services.set(IExtensionSignatureVerificationService, new SyncDescriptor(ExtensionSignatureVerificationService)); + services.set(IAllowedExtensionsService, new SyncDescriptor(AllowedExtensionsService)); services.set(INativeServerExtensionManagementService, new SyncDescriptor(ExtensionManagementService)); services.set(ILanguagePackService, new SyncDescriptor(NativeLanguagePackService)); diff --git a/src/vs/server/node/serverServices.ts b/src/vs/server/node/serverServices.ts index 42517aa71b3ad..930626375a68f 100644 --- a/src/vs/server/node/serverServices.ts +++ b/src/vs/server/node/serverServices.ts @@ -20,7 +20,7 @@ import { IDownloadService } from '../../platform/download/common/download.js'; import { DownloadServiceChannelClient } from '../../platform/download/common/downloadIpc.js'; import { IEnvironmentService, INativeEnvironmentService } from '../../platform/environment/common/environment.js'; import { ExtensionGalleryServiceWithNoStorageService } from '../../platform/extensionManagement/common/extensionGalleryService.js'; -import { IExtensionGalleryService } from '../../platform/extensionManagement/common/extensionManagement.js'; +import { IAllowedExtensionsService, IExtensionGalleryService } from '../../platform/extensionManagement/common/extensionManagement.js'; import { ExtensionSignatureVerificationService, IExtensionSignatureVerificationService } from '../../platform/extensionManagement/node/extensionSignatureVerificationService.js'; import { ExtensionManagementCLI } from '../../platform/extensionManagement/common/extensionManagementCLI.js'; import { ExtensionManagementChannel } from '../../platform/extensionManagement/common/extensionManagementIpc.js'; @@ -78,6 +78,7 @@ import { RemoteExtensionsScannerChannelName } from '../../platform/remote/common import { RemoteUserDataProfilesServiceChannel } from '../../platform/userDataProfile/common/userDataProfileIpc.js'; import { NodePtyHostStarter } from '../../platform/terminal/node/nodePtyHostStarter.js'; import { CSSDevelopmentService, ICSSDevelopmentService } from '../../platform/cssDev/node/cssDevService.js'; +import { AllowedExtensionsService } from '../../platform/extensionManagement/common/allowedExtensionsService.js'; const eventPrefix = 'monacoworkbench'; @@ -189,6 +190,7 @@ export async function setupServerServices(connectionToken: ServerConnectionToken services.set(IExtensionsProfileScannerService, new SyncDescriptor(ExtensionsProfileScannerService)); services.set(IExtensionsScannerService, new SyncDescriptor(ExtensionsScannerService)); services.set(IExtensionSignatureVerificationService, new SyncDescriptor(ExtensionSignatureVerificationService)); + services.set(IAllowedExtensionsService, new SyncDescriptor(AllowedExtensionsService)); services.set(INativeServerExtensionManagementService, new SyncDescriptor(ExtensionManagementService)); const instantiationService: IInstantiationService = new InstantiationService(services); diff --git a/src/vs/workbench/api/test/browser/extHostTelemetry.test.ts b/src/vs/workbench/api/test/browser/extHostTelemetry.test.ts index 7be5b9554c558..476023a471289 100644 --- a/src/vs/workbench/api/test/browser/extHostTelemetry.test.ts +++ b/src/vs/workbench/api/test/browser/extHostTelemetry.test.ts @@ -66,6 +66,7 @@ suite('ExtHostTelemetry', function () { engines: { vscode: '*' }, extensionLocation: URI.parse('fake'), enabledApiProposals: undefined, + preRelease: false, }; const createExtHostTelemetry = () => { diff --git a/src/vs/workbench/api/test/common/extHostExtensionActivator.test.ts b/src/vs/workbench/api/test/common/extHostExtensionActivator.test.ts index f5c64b4444d29..c13ac061a768f 100644 --- a/src/vs/workbench/api/test/common/extHostExtensionActivator.test.ts +++ b/src/vs/workbench/api/test/common/extHostExtensionActivator.test.ts @@ -277,6 +277,7 @@ suite('ExtensionsActivator', () => { targetPlatform: TargetPlatform.UNDEFINED, extensionDependencies: deps.map(d => d.value), enabledApiProposals: undefined, + preRelease: false, }; } diff --git a/src/vs/workbench/contrib/debug/test/node/debugger.test.ts b/src/vs/workbench/contrib/debug/test/node/debugger.test.ts index 9df2d6e6e37d3..73f5b3a2396bc 100644 --- a/src/vs/workbench/contrib/debug/test/node/debugger.test.ts +++ b/src/vs/workbench/contrib/debug/test/node/debugger.test.ts @@ -66,6 +66,7 @@ suite('Debug - Debugger', () => { ] }, enabledApiProposals: undefined, + preRelease: false, }; const extensionDescriptor1 = { @@ -92,6 +93,7 @@ suite('Debug - Debugger', () => { ] }, enabledApiProposals: undefined, + preRelease: false, }; const extensionDescriptor2 = { @@ -126,6 +128,7 @@ suite('Debug - Debugger', () => { ] }, enabledApiProposals: undefined, + preRelease: false, }; diff --git a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts index 33cbc92262108..27bfbd1d01093 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts @@ -8,7 +8,7 @@ import { KeyMod, KeyCode } from '../../../../base/common/keyCodes.js'; import { Registry } from '../../../../platform/registry/common/platform.js'; import { MenuRegistry, MenuId, registerAction2, Action2, IMenuItem, IAction2Options } from '../../../../platform/actions/common/actions.js'; import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; -import { ExtensionsLocalizedLabel, IExtensionManagementService, IExtensionGalleryService, PreferencesLocalizedLabel, EXTENSION_INSTALL_SOURCE_CONTEXT, ExtensionInstallSource, UseUnpkgResourceApi } from '../../../../platform/extensionManagement/common/extensionManagement.js'; +import { ExtensionsLocalizedLabel, IExtensionManagementService, IExtensionGalleryService, PreferencesLocalizedLabel, EXTENSION_INSTALL_SOURCE_CONTEXT, ExtensionInstallSource, UseUnpkgResourceApiConfigKey, AllowedExtensionsConfigKey } from '../../../../platform/extensionManagement/common/extensionManagement.js'; import { EnablementState, IExtensionManagementServerService, IWorkbenchExtensionEnablementService, IWorkbenchExtensionManagementService, extensionsConfigurationNodeBase } from '../../../services/extensionManagement/common/extensionManagement.js'; import { IExtensionIgnoredRecommendationsService, IExtensionRecommendationsService } from '../../../services/extensionRecommendations/common/extensionRecommendations.js'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from '../../../common/contributions.js'; @@ -261,12 +261,72 @@ Registry.as(ConfigurationExtensions.Configuration) scope: ConfigurationScope.APPLICATION, included: isNative && !isLinux }, - [UseUnpkgResourceApi]: { + [UseUnpkgResourceApiConfigKey]: { type: 'boolean', description: localize('extensions.gallery.useUnpkgResourceApi', "When enabled, extensions to update are fetched from Unpkg service."), default: true, scope: ConfigurationScope.APPLICATION, tags: ['onExp', 'usesOnlineServices'] + }, + [AllowedExtensionsConfigKey]: { + // Note: Type is set only to object because to support policies generation during build time, where single type is expected. + type: 'object', + description: localize('extensions.allowed', "List of extensions that are allowed."), + default: '*', + defaultSnippets: [{ + body: {}, + description: localize('extensions.allowed.none', "No extensions are allowed."), + }, { + body: { + '*': true + }, + description: localize('extensions.allowed.all', "All extensions are allowed."), + }], + scope: ConfigurationScope.APPLICATION, + policy: { + name: 'AllowedExtensions', + minimumVersion: '1.96', + }, + additionalProperties: false, + patternProperties: { + '([a-z0-9A-Z][a-z0-9-A-Z]*)\\.([a-z0-9A-Z][a-z0-9-A-Z]*)$': { + anyOf: [ + { + type: ['boolean', 'string'], + enum: [true, false, 'stable'], + description: localize('extensions.allow.description', "Allow or disallow the extension."), + enumDescriptions: [ + localize('extensions.allowed.enable.desc', "Extension is allowed."), + localize('extensions.allowed.disable.desc', "Extension is not allowed."), + localize('extensions.allowed.disable.stable.desc', "Allow only stable versions of the extension."), + ], + }, + { + type: 'array', + description: localize('extensions.allow.version.description', "Allow or disallow specific versions of the extension. To specifcy a platform specific version, use the format `platform@1.2.3`, e.g. `win32-x64@1.2.3`. Supported platforms are `win32-x64`, `win32-arm64`, `linux-x64`, `linux-arm64`, `linux-armhf`, `alpine-x64`, `alpine-arm64`, `darwin-x64`, `darwin-arm64`"), + }, + ] + }, + '([a-z0-9A-Z][a-z0-9-A-Z]*)$': { + type: ['boolean', 'string'], + enum: [true, false, 'stable'], + description: localize('extension.publisher.allow.description', "Allow or disallow all extensions from the publisher."), + enumDescriptions: [ + localize('extensions.publisher.allowed.enable.desc', "All extensions from the publisher are allowed."), + localize('extensions.publisher.allowed.disable.desc', "All extensions from the publisher are not allowed."), + localize('extensions.publisher.allowed.disable.stable.desc', "Allow only stable versions of the extensions from the publisher."), + ], + }, + '\\*': { + type: 'boolean', + enum: [true, false], + description: localize('extensions.allow.all.description', "Allow or disallow all extensions."), + enumDescriptions: [ + localize('extensions.allow.all.enable', "Allow all extensions."), + localize('extensions.allow.all.disable', "Disallow all extensions.") + ], + } + } } } }); @@ -1312,7 +1372,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi id: MenuId.ExtensionContext, group: INSTALL_ACTIONS_GROUP, order: 0, - when: ContextKeyExpr.and(ContextKeyExpr.has('inExtensionEditor'), ContextKeyExpr.has('galleryExtensionHasPreReleaseVersion'), ContextKeyExpr.not('showPreReleaseVersion'), ContextKeyExpr.not('isBuiltinExtension')) + when: ContextKeyExpr.and(ContextKeyExpr.has('inExtensionEditor'), ContextKeyExpr.has('galleryExtensionHasPreReleaseVersion'), ContextKeyExpr.has('isPreReleaseExtensionAllowed'), ContextKeyExpr.not('showPreReleaseVersion'), ContextKeyExpr.not('isBuiltinExtension')) }, run: async (accessor: ServicesAccessor, extensionId: string) => { const extensionWorkbenchService = accessor.get(IExtensionsWorkbenchService); @@ -1341,7 +1401,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi id: ToggleAutoUpdateForExtensionAction.ID, title: ToggleAutoUpdateForExtensionAction.LABEL, category: ExtensionsLocalizedLabel, - precondition: ContextKeyExpr.and(ContextKeyExpr.or(ContextKeyExpr.notEquals(`config.${AutoUpdateConfigurationKey}`, 'onlyEnabledExtensions'), ContextKeyExpr.equals('isExtensionEnabled', true)), ContextKeyExpr.not('extensionDisallowInstall')), + precondition: ContextKeyExpr.and(ContextKeyExpr.or(ContextKeyExpr.notEquals(`config.${AutoUpdateConfigurationKey}`, 'onlyEnabledExtensions'), ContextKeyExpr.equals('isExtensionEnabled', true)), ContextKeyExpr.not('extensionDisallowInstall'), ContextKeyExpr.has('isExtensionAllowed')), menu: { id: MenuId.ExtensionContext, group: UPDATE_ACTIONS_GROUP, @@ -1395,7 +1455,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi id: MenuId.ExtensionContext, group: INSTALL_ACTIONS_GROUP, order: 2, - when: ContextKeyExpr.and(CONTEXT_HAS_GALLERY, ContextKeyExpr.has('galleryExtensionHasPreReleaseVersion'), ContextKeyExpr.not('installedExtensionIsOptedToPreRelease'), ContextKeyExpr.not('inExtensionEditor'), ContextKeyExpr.equals('extensionStatus', 'installed'), ContextKeyExpr.not('isBuiltinExtension')) + when: ContextKeyExpr.and(CONTEXT_HAS_GALLERY, ContextKeyExpr.has('galleryExtensionHasPreReleaseVersion'), ContextKeyExpr.has('isPreReleaseExtensionAllowed'), ContextKeyExpr.not('installedExtensionIsOptedToPreRelease'), ContextKeyExpr.not('inExtensionEditor'), ContextKeyExpr.equals('extensionStatus', 'installed'), ContextKeyExpr.not('isBuiltinExtension')) }, run: async (accessor: ServicesAccessor, id: string) => { const instantiationService = accessor.get(IInstantiationService); @@ -1477,7 +1537,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi menu: { id: MenuId.ExtensionContext, group: '0_install', - when: ContextKeyExpr.and(ContextKeyExpr.equals('extensionStatus', 'uninstalled'), ContextKeyExpr.has('isGalleryExtension'), ContextKeyExpr.not('extensionDisallowInstall'), CONTEXT_SYNC_ENABLEMENT), + when: ContextKeyExpr.and(ContextKeyExpr.equals('extensionStatus', 'uninstalled'), ContextKeyExpr.has('isGalleryExtension'), ContextKeyExpr.has('isExtensionAllowed'), ContextKeyExpr.not('extensionDisallowInstall'), CONTEXT_SYNC_ENABLEMENT), order: 1 }, run: async (accessor: ServicesAccessor, extensionId: string) => { @@ -1501,7 +1561,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi menu: { id: MenuId.ExtensionContext, group: '0_install', - when: ContextKeyExpr.and(ContextKeyExpr.equals('extensionStatus', 'uninstalled'), ContextKeyExpr.has('isGalleryExtension'), ContextKeyExpr.has('extensionHasPreReleaseVersion'), ContextKeyExpr.not('extensionDisallowInstall'), CONTEXT_SYNC_ENABLEMENT), + when: ContextKeyExpr.and(ContextKeyExpr.equals('extensionStatus', 'uninstalled'), ContextKeyExpr.has('isGalleryExtension'), ContextKeyExpr.has('extensionHasPreReleaseVersion'), ContextKeyExpr.has('isPreReleaseExtensionAllowed'), ContextKeyExpr.not('extensionDisallowInstall'), CONTEXT_SYNC_ENABLEMENT), order: 2 }, run: async (accessor: ServicesAccessor, extensionId: string) => { @@ -1525,7 +1585,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi menu: { id: MenuId.ExtensionContext, group: '0_install', - when: ContextKeyExpr.and(ContextKeyExpr.equals('extensionStatus', 'uninstalled'), ContextKeyExpr.has('isGalleryExtension'), ContextKeyExpr.not('extensionDisallowInstall')), + when: ContextKeyExpr.and(ContextKeyExpr.equals('extensionStatus', 'uninstalled'), ContextKeyExpr.has('isGalleryExtension'), ContextKeyExpr.has('isExtensionAllowed'), ContextKeyExpr.not('extensionDisallowInstall')), order: 3 }, run: async (accessor: ServicesAccessor, extensionId: string) => { diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts index 0cf14b097ffba..aeb14afeb98d6 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts @@ -14,7 +14,7 @@ import { IContextMenuService } from '../../../../platform/contextview/browser/co import { disposeIfDisposable } from '../../../../base/common/lifecycle.js'; import { IExtension, ExtensionState, IExtensionsWorkbenchService, IExtensionContainer, TOGGLE_IGNORE_EXTENSION_ACTION_ID, SELECT_INSTALL_VSIX_EXTENSION_COMMAND_ID, THEME_ACTIONS_GROUP, INSTALL_ACTIONS_GROUP, UPDATE_ACTIONS_GROUP, ExtensionEditorTab, ExtensionRuntimeActionType, IExtensionArg, AutoUpdateConfigurationKey } from '../common/extensions.js'; import { ExtensionsConfigurationInitialContent } from '../common/extensionsFileTemplate.js'; -import { IGalleryExtension, IExtensionGalleryService, ILocalExtension, InstallOptions, InstallOperation, ExtensionManagementErrorCode } from '../../../../platform/extensionManagement/common/extensionManagement.js'; +import { IGalleryExtension, IExtensionGalleryService, ILocalExtension, InstallOptions, InstallOperation, ExtensionManagementErrorCode, IAllowedExtensionsService } from '../../../../platform/extensionManagement/common/extensionManagement.js'; import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionManagementServer, IWorkbenchExtensionManagementService } from '../../../services/extensionManagement/common/extensionManagement.js'; import { ExtensionRecommendationReason, IExtensionIgnoredRecommendationsService, IExtensionRecommendationsService } from '../../../services/extensionRecommendations/common/extensionRecommendations.js'; import { areSameExtensions, getExtensionId } from '../../../../platform/extensionManagement/common/extensionManagementUtil.js'; @@ -411,11 +411,13 @@ export class InstallAction extends ExtensionAction { @IPreferencesService private readonly preferencesService: IPreferencesService, @ITelemetryService private readonly telemetryService: ITelemetryService, @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, + @IAllowedExtensionsService private readonly allowedExtensionsService: IAllowedExtensionsService, ) { super('extensions.install', localize('install', "Install"), InstallAction.CLASS, false); this.hideOnDisabled = false; this.options = { isMachineScoped: false, ...options }; this.update(); + this._register(allowedExtensionsService.onDidChangeAllowedExtensions(() => this.update())); this._register(this.labelService.onDidChangeFormatters(() => this.updateLabel(), this)); } @@ -439,7 +441,7 @@ export class InstallAction extends ExtensionAction { if (this.extension.state !== ExtensionState.Uninstalled) { return; } - if (this.options.installPreReleaseVersion && !this.extension.hasPreReleaseVersion) { + if (this.options.installPreReleaseVersion && (!this.extension.hasPreReleaseVersion || this.allowedExtensionsService.isAllowed({ id: this.extension.identifier.id, prerelease: true }) !== true)) { return; } if (!this.options.installPreReleaseVersion && !this.extension.hasReleaseVersion) { @@ -1013,6 +1015,7 @@ export class ToggleAutoUpdateForExtensionAction extends ExtensionAction { constructor( @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, @IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService, + @IAllowedExtensionsService private readonly allowedExtensionsService: IAllowedExtensionsService, @IConfigurationService configurationService: IConfigurationService, ) { super(ToggleAutoUpdateForExtensionAction.ID, ToggleAutoUpdateForExtensionAction.LABEL.value, ToggleAutoUpdateForExtensionAction.DisabledClass); @@ -1021,6 +1024,7 @@ export class ToggleAutoUpdateForExtensionAction extends ExtensionAction { this.update(); } })); + this._register(allowedExtensionsService.onDidChangeAllowedExtensions(e => this.update())); this.update(); } @@ -1036,6 +1040,10 @@ export class ToggleAutoUpdateForExtensionAction extends ExtensionAction { if (this.extension.deprecationInfo?.disallowInstall) { return; } + const extension = this.extension.local ?? this.extension.gallery; + if (extension && this.allowedExtensionsService.isAllowed(extension) !== true) { + return; + } if (this.extensionsWorkbenchService.getAutoUpdateValue() === 'onlyEnabledExtensions' && !this.extensionEnablementService.isEnabledEnablementState(this.extension.enablementState)) { return; } @@ -1200,6 +1208,7 @@ async function getContextMenuActionsGroups(extension: IExtension | undefined | n const extensionIgnoredRecommendationsService = accessor.get(IExtensionIgnoredRecommendationsService); const workbenchThemeService = accessor.get(IWorkbenchThemeService); const authenticationUsageService = accessor.get(IAuthenticationUsageService); + const allowedExtensionsService = accessor.get(IAllowedExtensionsService); const cksOverlay: [string, any][] = []; if (extension) { @@ -1241,6 +1250,8 @@ async function getContextMenuActionsGroups(extension: IExtension | undefined | n cksOverlay.push(['extensionHasPreReleaseVersion', extension.hasPreReleaseVersion]); cksOverlay.push(['extensionHasReleaseVersion', extension.hasReleaseVersion]); cksOverlay.push(['extensionDisallowInstall', !!extension.deprecationInfo?.disallowInstall]); + cksOverlay.push(['isExtensionAllowed', allowedExtensionsService.isAllowed({ id: extension.identifier.id }) === true]); + cksOverlay.push(['isPreReleaseExtensionAllowed', allowedExtensionsService.isAllowed({ id: extension.identifier.id, prerelease: true }) === true]); cksOverlay.push(['extensionIsUnsigned', extension.gallery && !extension.gallery.isSigned]); const [colorThemes, fileIconThemes, productIconThemes, extensionUsesAuth] = await Promise.all([workbenchThemeService.getColorThemes(), workbenchThemeService.getFileIconThemes(), workbenchThemeService.getProductIconThemes(), authenticationUsageService.extensionUsesAuth(extension.identifier.id.toLowerCase())]); @@ -2486,6 +2497,7 @@ export class ExtensionStatusAction extends ExtensionAction { @IExtensionManifestPropertiesService private readonly extensionManifestPropertiesService: IExtensionManifestPropertiesService, @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, @IProductService private readonly productService: IProductService, + @IAllowedExtensionsService private readonly allowedExtensionsService: IAllowedExtensionsService, @IWorkbenchExtensionEnablementService private readonly workbenchExtensionEnablementService: IWorkbenchExtensionEnablementService, @IExtensionFeaturesManagementService private readonly extensionFeaturesManagementService: IExtensionFeaturesManagementService, ) { @@ -2493,6 +2505,7 @@ export class ExtensionStatusAction extends ExtensionAction { this._register(this.labelService.onDidChangeFormatters(() => this.update(), this)); this._register(this.extensionService.onDidChangeExtensions(() => this.update())); this._register(this.extensionFeaturesManagementService.onDidChangeAccessData(() => this.update())); + this._register(allowedExtensionsService.onDidChangeAllowedExtensions(() => this.update())); this.update(); } @@ -2571,6 +2584,13 @@ export class ExtensionStatusAction extends ExtensionAction { return; } + // Extension is disabled by its dependency + const result = this.allowedExtensionsService.isAllowed(this.extension.local); + if (result !== true) { + this.updateStatus({ icon: warningIcon, message: new MarkdownString(localize('disabled - not allowed', "This extension is disabled because {0}", result.value)) }, true); + return; + } + // Extension is disabled by environment if (this.extension.enablementState === EnablementState.DisabledByEnvironment) { this.updateStatus({ message: new MarkdownString(localize('disabled by environment', "This extension is disabled by the environment.")) }, true); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts index 3f55d68d56c09..c6ea38632404f 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts @@ -18,7 +18,8 @@ import { IExtensionsControlManifest, IExtensionInfo, IExtensionQueryOptions, IDeprecationInfo, isTargetPlatformCompatible, InstallExtensionInfo, EXTENSION_IDENTIFIER_REGEX, InstallOptions, IProductVersion, UninstallExtensionInfo, - TargetPlatformToString + TargetPlatformToString, + IAllowedExtensionsService } from '../../../../platform/extensionManagement/common/extensionManagement.js'; import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionManagementServer, IWorkbenchExtensionManagementService, DefaultIconPath, IResourceExtension, extensionsConfigurationNodeBase } from '../../../services/extensionManagement/common/extensionManagement.js'; import { getGalleryExtensionTelemetryData, getLocalExtensionTelemetryData, areSameExtensions, groupByExtension, getGalleryExtensionId } from '../../../../platform/extensionManagement/common/extensionManagementUtil.js'; @@ -958,6 +959,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension @IViewsService private readonly viewsService: IViewsService, @IFileDialogService private readonly fileDialogService: IFileDialogService, @IQuickInputService private readonly quickInputService: IQuickInputService, + @IAllowedExtensionsService private readonly allowedExtensionsService: IAllowedExtensionsService, ) { super(); const preferPreReleasesValue = configurationService.getValue('_extensions.preferPreReleases'); @@ -1087,6 +1089,12 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension } })); + this._register(this.allowedExtensionsService.onDidChangeAllowedExtensions(() => { + if (this.isAutoCheckUpdatesEnabled()) { + this.checkForUpdates(); + } + })); + // Update AutoUpdate Contexts this.hasOutdatedExtensionsContextKey.set(this.outdated.length > 0); @@ -1418,6 +1426,16 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension }); } + const disallowedExtensions = this.local.filter(e => e.enablementState === EnablementState.DisabledByAllowlist); + if (disallowedExtensions.length) { + computedNotificiations.push({ + message: nls.localize('disallowed extensions', "Some extensions are disabled because they are configured not to be in the allowed list."), + severity: Severity.Warning, + extensions: disallowedExtensions, + key: 'disallowedExtensions:' + disallowedExtensions.sort((a, b) => a.identifier.id.localeCompare(b.identifier.id)).map(e => e.identifier.id.toLowerCase()).join('-'), + }); + } + return computedNotificiations; } diff --git a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionRecommendationsService.test.ts b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionRecommendationsService.test.ts index 6239a1dcd878d..bff7df509c71f 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionRecommendationsService.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionRecommendationsService.test.ts @@ -230,7 +230,7 @@ suite('ExtensionRecommendationsService Test', () => { onDidChangeProfile: Event.None, async getInstalled() { return []; }, async canInstall() { return true; }, - async getExtensionsControlManifest() { return { malicious: [], deprecated: {}, search: [] }; }, + async getExtensionsControlManifest() { return { malicious: [], deprecated: {}, search: [], publisherMapping: {} }; }, async getTargetPlatform() { return getTargetPlatform(platform, arch); }, }); instantiationService.stub(IExtensionService, { diff --git a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsActions.test.ts b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsActions.test.ts index 5eb95e8729642..37349a68d762c 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsActions.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsActions.test.ts @@ -103,7 +103,7 @@ function setupTest(disposables: Pick) { onDidChangeProfile: Event.None, async getInstalled() { return []; }, async getInstalledWorkspaceExtensions() { return []; }, - async getExtensionsControlManifest() { return { malicious: [], deprecated: {}, search: [] }; }, + async getExtensionsControlManifest() { return { malicious: [], deprecated: {}, search: [], publisherMapping: {} }; }, async updateMetadata(local: Mutable, metadata: Partial) { local.identifier.uuid = metadata.id; local.publisherDisplayName = metadata.publisherDisplayName!; @@ -2650,6 +2650,6 @@ function createExtensionManagementService(installed: ILocalExtension[] = []): IP return local; }, async getTargetPlatform() { return getTargetPlatform(platform, arch); }, - async getExtensionsControlManifest() { return { malicious: [], deprecated: {}, search: [] }; }, + async getExtensionsControlManifest() { return { malicious: [], deprecated: {}, search: [], publisherMapping: {} }; }, }; } diff --git a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsViews.test.ts b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsViews.test.ts index 0e995dbf1538b..19268a3cecd74 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsViews.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsViews.test.ts @@ -104,7 +104,7 @@ suite('ExtensionsViews Tests', () => { async getInstalled() { return []; }, async getInstalledWorkspaceExtensions() { return []; }, async canInstall() { return true; }, - async getExtensionsControlManifest() { return { malicious: [], deprecated: {}, search: [] }; }, + async getExtensionsControlManifest() { return { malicious: [], deprecated: {}, search: [], publisherMapping: {} }; }, async getTargetPlatform() { return getTargetPlatform(platform, arch); }, async updateMetadata(local) { return local; } }); diff --git a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsWorkbenchService.test.ts b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsWorkbenchService.test.ts index 3283dc88cb408..03fd27f96b8ff 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsWorkbenchService.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsWorkbenchService.test.ts @@ -104,7 +104,7 @@ suite('ExtensionsWorkbenchServiceTest', () => { onDidChangeProfile: Event.None, async getInstalled() { return []; }, async getInstalledWorkspaceExtensions() { return []; }, - async getExtensionsControlManifest() { return { malicious: [], deprecated: {}, search: [] }; }, + async getExtensionsControlManifest() { return { malicious: [], deprecated: {}, search: [], publisherMapping: {} }; }, async updateMetadata(local: Mutable, metadata: Partial) { local.identifier.uuid = metadata.id; local.publisherDisplayName = metadata.publisherDisplayName!; @@ -1712,7 +1712,7 @@ suite('ExtensionsWorkbenchServiceTest', () => { return local; }, getTargetPlatform: async () => getTargetPlatform(platform, arch), - async getExtensionsControlManifest() { return { malicious: [], deprecated: {}, search: [] }; }, + async getExtensionsControlManifest() { return { malicious: [], deprecated: {}, search: [], publisherMapping: {} }; }, async resetPinnedStateForAllUserExtensions(pinned: boolean) { } }; } diff --git a/src/vs/workbench/services/extensionManagement/browser/builtinExtensionsScannerService.ts b/src/vs/workbench/services/extensionManagement/browser/builtinExtensionsScannerService.ts index 97de43c306079..dae14c4d56265 100644 --- a/src/vs/workbench/services/extensionManagement/browser/builtinExtensionsScannerService.ts +++ b/src/vs/workbench/services/extensionManagement/browser/builtinExtensionsScannerService.ts @@ -77,7 +77,8 @@ export class BuiltinExtensionsScannerService implements IBuiltinExtensionsScanne changelogUrl: e.changelogPath ? uriIdentityService.extUri.joinPath(builtinExtensionsServiceUrl, e.changelogPath) : undefined, targetPlatform: TargetPlatform.WEB, validations: [], - isValid: true + isValid: true, + preRelease: false, }; }); } diff --git a/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts b/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts index 3b4a3c04c4f88..0a909924e6076 100644 --- a/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts +++ b/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts @@ -6,13 +6,13 @@ import { localize } from '../../../../nls.js'; import { Event, Emitter } from '../../../../base/common/event.js'; import { Disposable, toDisposable } from '../../../../base/common/lifecycle.js'; -import { IExtensionManagementService, IExtensionIdentifier, IGlobalExtensionEnablementService, ENABLED_EXTENSIONS_STORAGE_PATH, DISABLED_EXTENSIONS_STORAGE_PATH, InstallOperation } from '../../../../platform/extensionManagement/common/extensionManagement.js'; +import { IExtensionManagementService, IExtensionIdentifier, IGlobalExtensionEnablementService, ENABLED_EXTENSIONS_STORAGE_PATH, DISABLED_EXTENSIONS_STORAGE_PATH, InstallOperation, IAllowedExtensionsService } from '../../../../platform/extensionManagement/common/extensionManagement.js'; import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IWorkbenchExtensionManagementService, IExtensionManagementServer, ExtensionInstallLocation } from '../common/extensionManagement.js'; import { areSameExtensions, BetterMergeId, getExtensionDependencies } from '../../../../platform/extensionManagement/common/extensionManagementUtil.js'; import { IWorkspaceContextService, WorkbenchState } from '../../../../platform/workspace/common/workspace.js'; import { IStorageService, StorageScope } from '../../../../platform/storage/common/storage.js'; import { IWorkbenchEnvironmentService } from '../../environment/common/environmentService.js'; -import { IExtension, isAuthenticationProviderExtension, isLanguagePackExtension, isResolverExtension } from '../../../../platform/extensions/common/extensions.js'; +import { ExtensionType, IExtension, isAuthenticationProviderExtension, isLanguagePackExtension, isResolverExtension } from '../../../../platform/extensions/common/extensions.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; import { StorageManager } from '../../../../platform/extensionManagement/common/extensionEnablementService.js'; @@ -42,7 +42,7 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench protected readonly extensionsManager: ExtensionsManager; private readonly storageManager: StorageManager; - private extensionsDisabledByExtensionDependency: IExtension[] = []; + private extensionsDisabledExtensions: IExtension[] = []; constructor( @IStorageService storageService: IStorageService, @@ -58,6 +58,7 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench @INotificationService private readonly notificationService: INotificationService, @IHostService hostService: IHostService, @IExtensionBisectService private readonly extensionBisectService: IExtensionBisectService, + @IAllowedExtensionsService private readonly allowedExtensionsService: IAllowedExtensionsService, @IWorkspaceTrustManagementService private readonly workspaceTrustManagementService: IWorkspaceTrustManagementService, @IWorkspaceTrustRequestService private readonly workspaceTrustRequestService: IWorkspaceTrustRequestService, @IExtensionManifestPropertiesService private readonly extensionManifestPropertiesService: IExtensionManifestPropertiesService, @@ -79,6 +80,7 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench }); this._register(this.globalExtensionEnablementService.onDidChangeEnablement(({ extensions, source }) => this._onDidChangeGloballyDisabledExtensions(extensions, source))); + this._register(allowedExtensionsService.onDidChangeAllowedExtensions(() => this._onDidChangeExtensions([], [], false))); // delay notification for extensions disabled until workbench restored if (this.allUserExtensionsDisabled) { @@ -163,6 +165,8 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench throw new Error(localize('cannot change enablement virtual workspace', "Cannot change enablement of {0} extension because it does not support virtual workspaces", extension.manifest.displayName || extension.identifier.id)); case EnablementState.DisabledByExtensionKind: throw new Error(localize('cannot change enablement extension kind', "Cannot change enablement of {0} extension because of its extension kind", extension.manifest.displayName || extension.identifier.id)); + case EnablementState.DisabledByAllowlist: + throw new Error(localize('cannot change disallowed extension enablement', "Cannot change enablement of {0} extension because it is disallowed", extension.manifest.displayName || extension.identifier.id)); case EnablementState.DisabledByInvalidExtension: throw new Error(localize('cannot change invalid extension enablement', "Cannot change enablement of {0} extension because of it is invalid", extension.manifest.displayName || extension.identifier.id)); case EnablementState.DisabledByExtensionDependency: @@ -336,7 +340,11 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench enablementState = this._getUserEnablementState(extension.identifier); const isEnabled = this.isEnabledEnablementState(enablementState); - if (isEnabled && !extension.isValid) { + if (isEnabled && extension.type === ExtensionType.User && this.allowedExtensionsService.isAllowed(extension) !== true) { + enablementState = EnablementState.DisabledByAllowlist; + } + + else if (isEnabled && !extension.isValid) { enablementState = EnablementState.DisabledByInvalidExtension; } @@ -636,15 +644,18 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench private _onDidChangeExtensions(added: ReadonlyArray, removed: ReadonlyArray, isProfileSwitch: boolean): void { const changedExtensions: IExtension[] = added.filter(e => !this.isEnabledEnablementState(this.getEnablementState(e))); - const existingExtensionsDisabledByExtensionDependency = this.extensionsDisabledByExtensionDependency; - this.extensionsDisabledByExtensionDependency = this.extensionsManager.extensions.filter(extension => this.getEnablementState(extension) === EnablementState.DisabledByExtensionDependency); - for (const extension of existingExtensionsDisabledByExtensionDependency) { - if (this.extensionsDisabledByExtensionDependency.every(e => !areSameExtensions(e.identifier, extension.identifier))) { + const existingDisabledExtensions = this.extensionsDisabledExtensions; + this.extensionsDisabledExtensions = this.extensionsManager.extensions.filter(extension => { + const enablementState = this.getEnablementState(extension); + return enablementState === EnablementState.DisabledByExtensionDependency || enablementState === EnablementState.DisabledByAllowlist; + }); + for (const extension of existingDisabledExtensions) { + if (this.extensionsDisabledExtensions.every(e => !areSameExtensions(e.identifier, extension.identifier))) { changedExtensions.push(extension); } } - for (const extension of this.extensionsDisabledByExtensionDependency) { - if (existingExtensionsDisabledByExtensionDependency.every(e => !areSameExtensions(e.identifier, extension.identifier))) { + for (const extension of this.extensionsDisabledExtensions) { + if (existingDisabledExtensions.every(e => !areSameExtensions(e.identifier, extension.identifier))) { changedExtensions.push(extension); } } diff --git a/src/vs/workbench/services/extensionManagement/browser/webExtensionsScannerService.ts b/src/vs/workbench/services/extensionManagement/browser/webExtensionsScannerService.ts index c03d9ff032a00..529aa4e6dbdd4 100644 --- a/src/vs/workbench/services/extensionManagement/browser/webExtensionsScannerService.ts +++ b/src/vs/workbench/services/extensionManagement/browser/webExtensionsScannerService.ts @@ -793,7 +793,8 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten metadata: webExtension.metadata, targetPlatform: TargetPlatform.WEB, validations, - isValid + isValid, + preRelease: !!webExtension.metadata?.preRelease, }; } diff --git a/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts b/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts index 906d99dc5acd4..9028a14502044 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts @@ -103,6 +103,7 @@ export const enum EnablementState { EnabledByEnvironment, DisabledByVirtualWorkspace, DisabledByInvalidExtension, + DisabledByAllowlist, DisabledByExtensionDependency, DisabledGlobally, DisabledWorkspace, diff --git a/src/vs/workbench/services/extensionManagement/common/extensionManagementChannelClient.ts b/src/vs/workbench/services/extensionManagement/common/extensionManagementChannelClient.ts index f8ff8d73b911d..85b6b47cd9e7e 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionManagementChannelClient.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionManagementChannelClient.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ILocalExtension, IGalleryExtension, InstallOptions, UninstallOptions, Metadata, InstallExtensionResult, InstallExtensionInfo, IProductVersion, UninstallExtensionInfo, DidUninstallExtensionEvent, DidUpdateExtensionMetadata, InstallExtensionEvent, UninstallExtensionEvent } from '../../../../platform/extensionManagement/common/extensionManagement.js'; +import { ILocalExtension, IGalleryExtension, InstallOptions, UninstallOptions, Metadata, InstallExtensionResult, InstallExtensionInfo, IProductVersion, UninstallExtensionInfo, DidUninstallExtensionEvent, DidUpdateExtensionMetadata, InstallExtensionEvent, UninstallExtensionEvent, IAllowedExtensionsService } from '../../../../platform/extensionManagement/common/extensionManagement.js'; import { URI } from '../../../../base/common/uri.js'; import { ExtensionIdentifier, ExtensionType, IExtensionIdentifier } from '../../../../platform/extensions/common/extensions.js'; import { ExtensionManagementChannelClient as BaseExtensionManagementChannelClient } from '../../../../platform/extensionManagement/common/extensionManagementIpc.js'; @@ -32,10 +32,11 @@ export abstract class ProfileAwareExtensionManagementChannelClient extends BaseE constructor(channel: IChannel, productService: IProductService, + allowedExtensionsService: IAllowedExtensionsService, protected readonly userDataProfileService: IUserDataProfileService, protected readonly uriIdentityService: IUriIdentityService, ) { - super(channel, productService); + super(channel, productService, allowedExtensionsService); this._register(userDataProfileService.onDidChangeCurrentProfile(e => { if (!this.uriIdentityService.extUri.isEqual(e.previous.extensionsResource, e.profile.extensionsResource)) { e.join(this.whenProfileChanged(e)); diff --git a/src/vs/workbench/services/extensionManagement/common/remoteExtensionManagementService.ts b/src/vs/workbench/services/extensionManagement/common/remoteExtensionManagementService.ts index c77f5b649a193..028bfa2816a85 100644 --- a/src/vs/workbench/services/extensionManagement/common/remoteExtensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/common/remoteExtensionManagementService.ts @@ -13,18 +13,20 @@ import { IUserDataProfileService } from '../../userDataProfile/common/userDataPr import { IUserDataProfilesService } from '../../../../platform/userDataProfile/common/userDataProfile.js'; import { ExtensionIdentifier } from '../../../../platform/extensions/common/extensions.js'; import { IProductService } from '../../../../platform/product/common/productService.js'; +import { IAllowedExtensionsService } from '../../../../platform/extensionManagement/common/extensionManagement.js'; export class RemoteExtensionManagementService extends ProfileAwareExtensionManagementChannelClient implements IProfileAwareExtensionManagementService { constructor( channel: IChannel, @IProductService productService: IProductService, + @IAllowedExtensionsService allowedExtensionsService: IAllowedExtensionsService, @IUserDataProfileService userDataProfileService: IUserDataProfileService, @IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService, @IRemoteUserDataProfilesService private readonly remoteUserDataProfilesService: IRemoteUserDataProfilesService, @IUriIdentityService uriIdentityService: IUriIdentityService ) { - super(channel, productService, userDataProfileService, uriIdentityService); + super(channel, productService, allowedExtensionsService, userDataProfileService, uriIdentityService); } protected async filterEvent(profileLocation: URI, applicationScoped: boolean): Promise { diff --git a/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts b/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts index dc02d1a4e72e8..30971801a829b 100644 --- a/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { ExtensionIdentifier, ExtensionType, IExtension, IExtensionIdentifier, IExtensionManifest, TargetPlatform } from '../../../../platform/extensions/common/extensions.js'; -import { ILocalExtension, IGalleryExtension, InstallOperation, IExtensionGalleryService, Metadata, InstallOptions, IProductVersion } from '../../../../platform/extensionManagement/common/extensionManagement.js'; +import { ILocalExtension, IGalleryExtension, InstallOperation, IExtensionGalleryService, Metadata, InstallOptions, IProductVersion, IAllowedExtensionsService } from '../../../../platform/extensionManagement/common/extensionManagement.js'; import { URI } from '../../../../base/common/uri.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { areSameExtensions, getGalleryExtensionId } from '../../../../platform/extensionManagement/common/extensionManagementUtil.js'; @@ -22,8 +22,6 @@ import { compare } from '../../../../base/common/strings.js'; import { IUserDataProfilesService } from '../../../../platform/userDataProfile/common/userDataProfile.js'; import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js'; import { DisposableStore } from '../../../../base/common/lifecycle.js'; -import { IMarkdownString, MarkdownString } from '../../../../base/common/htmlContent.js'; -import { localize } from '../../../../nls.js'; export class WebExtensionManagementService extends AbstractExtensionManagementService implements IProfileAwareExtensionManagementService { @@ -60,10 +58,11 @@ export class WebExtensionManagementService extends AbstractExtensionManagementSe @IExtensionManifestPropertiesService private readonly extensionManifestPropertiesService: IExtensionManifestPropertiesService, @IUserDataProfileService private readonly userDataProfileService: IUserDataProfileService, @IProductService productService: IProductService, + @IAllowedExtensionsService allowedExtensionsService: IAllowedExtensionsService, @IUserDataProfilesService userDataProfilesService: IUserDataProfilesService, @IUriIdentityService uriIdentityService: IUriIdentityService, ) { - super(extensionGalleryService, telemetryService, uriIdentityService, logService, productService, userDataProfilesService); + super(extensionGalleryService, telemetryService, uriIdentityService, logService, productService, allowedExtensionsService, userDataProfilesService); this._register(userDataProfileService.onDidChangeCurrentProfile(e => { if (!this.uriIdentityService.extUri.isEqual(e.previous.extensionsResource, e.profile.extensionsResource)) { e.join(this.whenProfileChanged(e)); @@ -80,15 +79,11 @@ export class WebExtensionManagementService extends AbstractExtensionManagementSe return TargetPlatform.WEB; } - override async canInstall(gallery: IGalleryExtension): Promise { - if (await super.canInstall(gallery) === true) { - return true; - } - if (this.isConfiguredToExecuteOnWeb(gallery)) { + protected override async isExtensionPlatformCompatible(extension: IGalleryExtension): Promise { + if (this.isConfiguredToExecuteOnWeb(extension)) { return true; } - const productName = localize('VS Code for Web', "{0} for the Web", this.productService.nameLong); - return new MarkdownString(`${localize('not web tooltip', "The '{0}' extension is not available in {1}.", gallery.displayName || gallery.identifier.id, productName)} [${localize('learn why', "Learn Why")}](https://aka.ms/vscode-web-extensions-guide)`); + return super.isExtensionPlatformCompatible(extension); } async getInstalled(type?: ExtensionType, profileLocation?: URI): Promise { @@ -235,7 +230,7 @@ function toLocalExtension(extension: IExtension): ILocalExtension { installedTimestamp: metadata.installedTimestamp, isPreReleaseVersion: !!metadata.isPreReleaseVersion, hasPreReleaseVersion: !!metadata.hasPreReleaseVersion, - preRelease: !!metadata.preRelease, + preRelease: extension.preRelease, targetPlatform: TargetPlatform.WEB, updated: !!metadata.updated, pinned: !!metadata?.pinned, diff --git a/src/vs/workbench/services/extensionManagement/electron-sandbox/nativeExtensionManagementService.ts b/src/vs/workbench/services/extensionManagement/electron-sandbox/nativeExtensionManagementService.ts index 4f71691689558..b45ca0bcce644 100644 --- a/src/vs/workbench/services/extensionManagement/electron-sandbox/nativeExtensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/electron-sandbox/nativeExtensionManagementService.ts @@ -6,7 +6,7 @@ import { IChannel } from '../../../../base/parts/ipc/common/ipc.js'; import { DidChangeProfileEvent, IProfileAwareExtensionManagementService } from '../common/extensionManagement.js'; import { URI } from '../../../../base/common/uri.js'; -import { ILocalExtension, InstallOptions } from '../../../../platform/extensionManagement/common/extensionManagement.js'; +import { IAllowedExtensionsService, ILocalExtension, InstallOptions } from '../../../../platform/extensionManagement/common/extensionManagement.js'; import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js'; import { IUserDataProfileService } from '../../userDataProfile/common/userDataProfile.js'; import { joinPath } from '../../../../base/common/resources.js'; @@ -25,6 +25,7 @@ export class NativeExtensionManagementService extends ProfileAwareExtensionManag constructor( channel: IChannel, @IProductService productService: IProductService, + @IAllowedExtensionsService allowedExtensionsService: IAllowedExtensionsService, @IUserDataProfileService userDataProfileService: IUserDataProfileService, @IUriIdentityService uriIdentityService: IUriIdentityService, @IFileService private readonly fileService: IFileService, @@ -32,7 +33,7 @@ export class NativeExtensionManagementService extends ProfileAwareExtensionManag @INativeWorkbenchEnvironmentService private readonly nativeEnvironmentService: INativeWorkbenchEnvironmentService, @ILogService private readonly logService: ILogService, ) { - super(channel, productService, userDataProfileService, uriIdentityService); + super(channel, productService, allowedExtensionsService, userDataProfileService, uriIdentityService); } protected filterEvent(profileLocation: URI, isApplicationScoped: boolean): boolean { diff --git a/src/vs/workbench/services/extensionManagement/electron-sandbox/remoteExtensionManagementService.ts b/src/vs/workbench/services/extensionManagement/electron-sandbox/remoteExtensionManagementService.ts index 6d0db640b30aa..f8fb1fd55ea3c 100644 --- a/src/vs/workbench/services/extensionManagement/electron-sandbox/remoteExtensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/electron-sandbox/remoteExtensionManagementService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { IChannel } from '../../../../base/parts/ipc/common/ipc.js'; -import { ILocalExtension, IGalleryExtension, IExtensionGalleryService, InstallOperation, InstallOptions, ExtensionManagementError, ExtensionManagementErrorCode, EXTENSION_INSTALL_CLIENT_TARGET_PLATFORM_CONTEXT } from '../../../../platform/extensionManagement/common/extensionManagement.js'; +import { ILocalExtension, IGalleryExtension, IExtensionGalleryService, InstallOperation, InstallOptions, ExtensionManagementError, ExtensionManagementErrorCode, EXTENSION_INSTALL_CLIENT_TARGET_PLATFORM_CONTEXT, IAllowedExtensionsService } from '../../../../platform/extensionManagement/common/extensionManagement.js'; import { URI } from '../../../../base/common/uri.js'; import { ExtensionType, IExtensionManifest } from '../../../../platform/extensions/common/extensions.js'; import { areSameExtensions } from '../../../../platform/extensionManagement/common/extensionManagementUtil.js'; @@ -40,10 +40,11 @@ export class NativeRemoteExtensionManagementService extends RemoteExtensionManag @ILogService private readonly logService: ILogService, @IExtensionGalleryService private readonly galleryService: IExtensionGalleryService, @IConfigurationService private readonly configurationService: IConfigurationService, + @IAllowedExtensionsService allowedExtensionsService: IAllowedExtensionsService, @IFileService private readonly fileService: IFileService, @IExtensionManifestPropertiesService private readonly extensionManifestPropertiesService: IExtensionManifestPropertiesService, ) { - super(channel, productService, userDataProfileService, userDataProfilesService, remoteUserDataProfilesService, uriIdentityService); + super(channel, productService, allowedExtensionsService, userDataProfileService, userDataProfilesService, remoteUserDataProfilesService, uriIdentityService); } override async install(vsix: URI, options?: InstallOptions): Promise { diff --git a/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts b/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts index 002fbc2613468..d5a9d45a0a062 100644 --- a/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts +++ b/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; import * as sinon from 'sinon'; -import { IExtensionManagementService, DidUninstallExtensionEvent, ILocalExtension, InstallExtensionEvent, InstallExtensionResult, UninstallExtensionEvent, DidUpdateExtensionMetadata, InstallOperation } from '../../../../../platform/extensionManagement/common/extensionManagement.js'; +import { IExtensionManagementService, DidUninstallExtensionEvent, ILocalExtension, InstallExtensionEvent, InstallExtensionResult, UninstallExtensionEvent, DidUpdateExtensionMetadata, InstallOperation, IAllowedExtensionsService, AllowedExtensionsConfigKey } from '../../../../../platform/extensionManagement/common/extensionManagement.js'; import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionManagementServer, IWorkbenchExtensionManagementService, ExtensionInstallLocation, IProfileAwareExtensionManagementService, DidChangeProfileEvent } from '../../common/extensionManagement.js'; import { ExtensionEnablementService } from '../../browser/extensionEnablementService.js'; import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; @@ -40,6 +40,7 @@ import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/tes import { IFileService } from '../../../../../platform/files/common/files.js'; import { FileService } from '../../../../../platform/files/common/fileService.js'; import { IProductService } from '../../../../../platform/product/common/productService.js'; +import { AllowedExtensionsService } from '../../../../../platform/extensionManagement/common/allowedExtensionsService.js'; function createStorageService(instantiationService: TestInstantiationService, disposableStore: DisposableStore): IStorageService { let service = instantiationService.get(IStorageService); @@ -90,6 +91,7 @@ export class TestExtensionEnablementService extends ExtensionEnablementService { instantiationService.get(INotificationService) || instantiationService.stub(INotificationService, new TestNotificationService()), instantiationService.get(IHostService), new class extends mock() { override isDisabledByBisect() { return false; } }, + instantiationService.stub(IAllowedExtensionsService, disposables.add(new AllowedExtensionsService(instantiationService.get(IProductService), instantiationService.get(IConfigurationService)))), workspaceTrustManagementService, new class extends mock() { override requestWorkspaceTrust(options?: WorkspaceTrustRequestOptions): Promise { return Promise.resolve(true); } }, instantiationService.get(IExtensionManifestPropertiesService) || instantiationService.stub(IExtensionManifestPropertiesService, disposables.add(new ExtensionManifestPropertiesService(TestProductService, new TestConfigurationService(), new TestWorkspaceTrustEnablementService(), new NullLogService()))), @@ -134,7 +136,9 @@ suite('ExtensionEnablementService Test', () => { instantiationService = disposableStore.add(new TestInstantiationService()); instantiationService.stub(IFileService, disposableStore.add(new FileService(new NullLogService()))); instantiationService.stub(IProductService, TestProductService); - instantiationService.stub(IConfigurationService, new TestConfigurationService()); + const testConfigurationService = new TestConfigurationService(); + testConfigurationService.setUserConfiguration(AllowedExtensionsConfigKey, { '*': true, 'unallowed': false }); + instantiationService.stub(IConfigurationService, testConfigurationService); instantiationService.stub(IWorkspaceContextService, new TestContextService()); instantiationService.stub(IExtensionManagementServerService, anExtensionManagementServerService({ id: 'local', @@ -1154,6 +1158,11 @@ suite('ExtensionEnablementService Test', () => { assert.deepStrictEqual((target.args[0][0][0]).identifier, { id: 'pub.a' }); }); + test('test extension is disabled by allowed list', async () => { + const target = aLocalExtension2('unallowed.extension'); + assert.strictEqual(testObject.getEnablementState(target), EnablementState.DisabledByAllowlist); + }); + }); function anExtensionManagementServer(authority: string, instantiationService: TestInstantiationService): IExtensionManagementServer { diff --git a/src/vs/workbench/services/extensions/common/extensions.ts b/src/vs/workbench/services/extensions/common/extensions.ts index d6b4204a7b9eb..cd50e10acfb7a 100644 --- a/src/vs/workbench/services/extensions/common/extensions.ts +++ b/src/vs/workbench/services/extensions/common/extensions.ts @@ -28,7 +28,8 @@ export const nullExtensionDescription = Object.freeze({ isBuiltin: false, targetPlatform: TargetPlatform.UNDEFINED, isUserBuiltin: false, - isUnderDevelopment: false + isUnderDevelopment: false, + preRelease: false, }); export type WebWorkerExtHostConfigValue = boolean | 'auto'; @@ -559,7 +560,8 @@ export function toExtension(extensionDescription: IExtensionDescription): IExten location: extensionDescription.extensionLocation, targetPlatform: extensionDescription.targetPlatform, validations: [], - isValid: true + isValid: true, + preRelease: extensionDescription.preRelease, }; } @@ -575,6 +577,7 @@ export function toExtensionDescription(extension: IExtension, isUnderDevelopment uuid: extension.identifier.uuid, targetPlatform: extension.targetPlatform, publisherDisplayName: extension.publisherDisplayName, + preRelease: extension.preRelease, ...extension.manifest }; } diff --git a/src/vs/workbench/services/extensions/test/common/extensionDescriptionRegistry.test.ts b/src/vs/workbench/services/extensions/test/common/extensionDescriptionRegistry.test.ts index e212c86e60c2c..aa9cc6c629d86 100644 --- a/src/vs/workbench/services/extensions/test/common/extensionDescriptionRegistry.test.ts +++ b/src/vs/workbench/services/extensions/test/common/extensionDescriptionRegistry.test.ts @@ -46,6 +46,7 @@ suite('ExtensionDescriptionRegistry', () => { targetPlatform: TargetPlatform.UNDEFINED, extensionDependencies: [], enabledApiProposals: undefined, + preRelease: false, }; } }); diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts index 4229f61ae24bd..de87fce30c3c7 100644 --- a/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -182,6 +182,7 @@ import { ContextMenuService } from '../../../platform/contextview/browser/contex import { IHoverService } from '../../../platform/hover/browser/hover.js'; import { NullHoverService } from '../../../platform/hover/test/browser/nullHoverService.js'; import { IActionViewItemService, NullActionViewItemService } from '../../../platform/actions/browser/actionViewItemService.js'; +import { IMarkdownString } from '../../../base/common/htmlContent.js'; export function createFileEditorInput(instantiationService: IInstantiationService, resource: URI): FileEditorInput { return instantiationService.createInstance(FileEditorInput, resource, undefined, undefined, undefined, undefined, undefined, undefined); @@ -2211,6 +2212,7 @@ export class TestWorkbenchExtensionManagementService implements IWorkbenchExtens install(vsix: URI, options?: InstallOptions | undefined): Promise { throw new Error('Method not implemented.'); } + isAllowed(): true | IMarkdownString { return true; } async canInstall(extension: IGalleryExtension): Promise { return true; } installFromGallery(extension: IGalleryExtension, options?: InstallOptions | undefined): Promise { throw new Error('Method not implemented.'); diff --git a/src/vs/workbench/workbench.common.main.ts b/src/vs/workbench/workbench.common.main.ts index c33b094cae184..054f3e75a6d4f 100644 --- a/src/vs/workbench/workbench.common.main.ts +++ b/src/vs/workbench/workbench.common.main.ts @@ -124,7 +124,7 @@ import './services/editor/common/customEditorLabelService.js'; import { InstantiationType, registerSingleton } from '../platform/instantiation/common/extensions.js'; import { ExtensionGalleryService } from '../platform/extensionManagement/common/extensionGalleryService.js'; import { GlobalExtensionEnablementService } from '../platform/extensionManagement/common/extensionEnablementService.js'; -import { IExtensionGalleryService, IGlobalExtensionEnablementService } from '../platform/extensionManagement/common/extensionManagement.js'; +import { IAllowedExtensionsService, IExtensionGalleryService, IGlobalExtensionEnablementService } from '../platform/extensionManagement/common/extensionManagement.js'; import { ContextViewService } from '../platform/contextview/browser/contextViewService.js'; import { IContextViewService } from '../platform/contextview/browser/contextView.js'; import { IListService, ListService } from '../platform/list/browser/listService.js'; @@ -148,6 +148,7 @@ import { IUserDataSyncLogService } from '../platform/userDataSync/common/userDat import { UserDataSyncLogService } from '../platform/userDataSync/common/userDataSyncLog.js'; registerSingleton(IUserDataSyncLogService, UserDataSyncLogService, InstantiationType.Delayed); +registerSingleton(IAllowedExtensionsService, AllowedExtensionsService, InstantiationType.Delayed); registerSingleton(IIgnoredExtensionsManagementService, IgnoredExtensionsManagementService, InstantiationType.Delayed); registerSingleton(IGlobalExtensionEnablementService, GlobalExtensionEnablementService, InstantiationType.Delayed); registerSingleton(IExtensionStorageService, ExtensionStorageService, InstantiationType.Delayed); @@ -400,6 +401,7 @@ import './contrib/inlineCompletions/browser/inlineCompletions.contribution.js'; // Drop or paste into import './contrib/dropOrPasteInto/browser/dropOrPasteInto.contribution.js'; +import { AllowedExtensionsService } from '../platform/extensionManagement/common/allowedExtensionsService.js'; //#endregion From dff820317ba56096ad240f5f0dde0c1338310fa0 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Fri, 22 Nov 2024 14:07:17 +0100 Subject: [PATCH 058/119] Support drag and drop for symbols in breadcrumbs, outline and chat (#234372) * Support DnD for breadcrumbs and symbols * symbol data type * Refactor DocumentSymbolDragAndDrop to remove model provider dependency --- src/vs/platform/dnd/browser/dnd.ts | 28 +++++++- .../parts/editor/breadcrumbsControl.ts | 69 ++++++++++++++++++- .../contrib/chat/browser/chatDragAndDrop.ts | 40 ++++++++++- .../browser/outline/documentSymbolsOutline.ts | 5 +- .../browser/outline/documentSymbolsTree.ts | 69 ++++++++++++++++++- 5 files changed, 201 insertions(+), 10 deletions(-) diff --git a/src/vs/platform/dnd/browser/dnd.ts b/src/vs/platform/dnd/browser/dnd.ts index 83034d82cbfc6..c7add6739cbe6 100644 --- a/src/vs/platform/dnd/browser/dnd.ts +++ b/src/vs/platform/dnd/browser/dnd.ts @@ -29,7 +29,8 @@ import { Registry } from '../../registry/common/platform.js'; export const CodeDataTransfers = { EDITORS: 'CodeEditors', - FILES: 'CodeFiles' + FILES: 'CodeFiles', + SYMBOLS: 'application/vnd.code.symbols' }; export interface IDraggedResourceEditorInput extends IBaseTextResourceEditorInput { @@ -400,6 +401,31 @@ export class LocalSelectionTransfer { } } +export interface DocumentSymbolTransferData { + name: string; + fsPath: string; + range: { + startLineNumber: number; + startColumn: number; + endLineNumber: number; + endColumn: number; + }; + kind: number; +} + +export function extractSymbolDropData(e: DragEvent): DocumentSymbolTransferData[] { + const rawSymbolsData = e.dataTransfer?.getData(CodeDataTransfers.SYMBOLS); + if (rawSymbolsData) { + try { + return JSON.parse(rawSymbolsData); + } catch (error) { + // Invalid transfer + } + } + + return []; +} + /** * A helper to get access to Electrons `webUtils.getPathForFile` function * in a safe way without crashing the application when running in the web. diff --git a/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts b/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts index c976a9565e3ff..1b8d72b374517 100644 --- a/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts +++ b/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts @@ -8,8 +8,8 @@ import { StandardMouseEvent } from '../../../../base/browser/mouseEvent.js'; import { BreadcrumbsItem, BreadcrumbsWidget, IBreadcrumbsItemEvent, IBreadcrumbsWidgetStyles } from '../../../../base/browser/ui/breadcrumbs/breadcrumbsWidget.js'; import { timeout } from '../../../../base/common/async.js'; import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js'; -import { combinedDisposable, DisposableStore, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; -import { extUri } from '../../../../base/common/resources.js'; +import { combinedDisposable, DisposableStore, IDisposable, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; +import { basename, extUri } from '../../../../base/common/resources.js'; import { URI } from '../../../../base/common/uri.js'; import './media/breadcrumbscontrol.css'; import { localize, localize2 } from '../../../../nls.js'; @@ -40,6 +40,11 @@ import { defaultBreadcrumbsWidgetStyles } from '../../../../platform/theme/brows import { Emitter } from '../../../../base/common/event.js'; import { IHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegate.js'; import { getDefaultHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegateFactory.js'; +import { DataTransfers } from '../../../../base/browser/dnd.js'; +import { $ } from '../../../../base/browser/dom.js'; +import { OutlineElement } from '../../../../editor/contrib/documentSymbols/browser/outlineModel.js'; +import { CodeDataTransfers, DocumentSymbolTransferData } from '../../../../platform/dnd/browser/dnd.js'; +import { withSelection } from '../../../../platform/opener/common/opener.js'; class OutlineItem extends BreadcrumbsItem { @@ -96,8 +101,22 @@ class OutlineItem extends BreadcrumbsItem { }, 0, template, undefined); this._disposables.add(toDisposable(() => { renderer.disposeTemplate(template); })); - } + if (element instanceof OutlineElement && outline.uri) { + const symbolUri = withSelection(outline.uri, element.symbol.range); + const symbolTransferData: DocumentSymbolTransferData = { + name: element.symbol.name, + fsPath: outline.uri.fsPath, + range: element.symbol.range, + kind: element.symbol.kind + }; + const dataTransfers: DataTransfer[] = [ + [CodeDataTransfers.SYMBOLS, [symbolTransferData]], + [DataTransfers.RESOURCES, [symbolUri]] + ]; + this._disposables.add(createBreadcrumbDndObserver(container, element.symbol.name, symbolUri.toString(), dataTransfers)); + } + } } class FileItem extends BreadcrumbsItem { @@ -139,9 +158,53 @@ class FileItem extends BreadcrumbsItem { }); container.classList.add(FileKind[this.element.kind].toLowerCase()); this._disposables.add(label); + + const dataTransfers: DataTransfer[] = [ + [CodeDataTransfers.FILES, [this.element.uri.fsPath]], + [DataTransfers.RESOURCES, [this.element.uri.toString()]], + ]; + const dndObserver = createBreadcrumbDndObserver(container, basename(this.element.uri), this.element.uri.toString(), dataTransfers); + this._disposables.add(dndObserver); } } +type DataTransfer = [string, any[]]; + +function createBreadcrumbDndObserver(container: HTMLElement, label: string, textData: string, dataTransfers: DataTransfer[]): IDisposable { + container.draggable = true; + + return new dom.DragAndDropObserver(container, { + onDragStart: event => { + if (!event.dataTransfer) { + return; + } + + // Set data transfer + event.dataTransfer.effectAllowed = 'copyMove'; + event.dataTransfer.setData(DataTransfers.TEXT, textData); + for (const [type, data] of dataTransfers) { + event.dataTransfer.setData(type, JSON.stringify(data)); + } + + // Create drag image and remove when dropped + const dragImage = $('.monaco-drag-image'); + dragImage.textContent = label; + + const getDragImageContainer = (e: HTMLElement | null) => { + while (e && !e.classList.contains('monaco-workbench')) { + e = e.parentElement; + } + return e || container.ownerDocument; + }; + + const dragContainer = getDragImageContainer(container); + dragContainer.appendChild(dragImage); + event.dataTransfer.setDragImage(dragImage, -10, -10); + setTimeout(() => dragImage.remove(), 0); + } + }); +} + export interface IBreadcrumbsControlOptions { readonly showFileIcons: boolean; readonly showSymbolIcons: boolean; diff --git a/src/vs/workbench/contrib/chat/browser/chatDragAndDrop.ts b/src/vs/workbench/contrib/chat/browser/chatDragAndDrop.ts index 6236a1e752497..74f91ca5db7cc 100644 --- a/src/vs/workbench/contrib/chat/browser/chatDragAndDrop.ts +++ b/src/vs/workbench/contrib/chat/browser/chatDragAndDrop.ts @@ -12,8 +12,10 @@ import { IDisposable } from '../../../../base/common/lifecycle.js'; import { Mimes } from '../../../../base/common/mime.js'; import { basename, joinPath } from '../../../../base/common/resources.js'; import { URI } from '../../../../base/common/uri.js'; +import { IRange } from '../../../../editor/common/core/range.js'; +import { SymbolKinds } from '../../../../editor/common/languages.js'; import { localize } from '../../../../nls.js'; -import { containsDragType, extractEditorsDropData, IDraggedResourceEditorInput } from '../../../../platform/dnd/browser/dnd.js'; +import { CodeDataTransfers, containsDragType, DocumentSymbolTransferData, extractEditorsDropData, extractSymbolDropData, IDraggedResourceEditorInput } from '../../../../platform/dnd/browser/dnd.js'; import { FileType, IFileService, IFileSystemProvider } from '../../../../platform/files/common/files.js'; import { IThemeService, Themable } from '../../../../platform/theme/common/themeService.js'; import { EditorInput } from '../../../common/editor/editorInput.js'; @@ -26,7 +28,8 @@ enum ChatDragAndDropType { FILE_INTERNAL, FILE_EXTERNAL, FOLDER, - IMAGE + IMAGE, + SYMBOL } export class ChatDragAndDrop extends Themable { @@ -155,6 +158,8 @@ export class ChatDragAndDrop extends Themable { // This is an esstimation based on the datatransfer types/items if (this.isImageDnd(e)) { return this.extensionService.extensions.some(ext => isProposedApiEnabled(ext, 'chatReferenceBinaryData')) ? ChatDragAndDropType.IMAGE : undefined; + } else if (containsDragType(e, CodeDataTransfers.SYMBOLS)) { + return ChatDragAndDropType.SYMBOL; } else if (containsDragType(e, DataTransfers.FILES)) { return ChatDragAndDropType.FILE_EXTERNAL; } else if (containsDragType(e, DataTransfers.INTERNAL_URI_LIST)) { @@ -178,6 +183,7 @@ export class ChatDragAndDrop extends Themable { case ChatDragAndDropType.FILE_EXTERNAL: return localize('file', 'File'); case ChatDragAndDropType.FOLDER: return localize('folder', 'Folder'); case ChatDragAndDropType.IMAGE: return localize('image', 'Image'); + case ChatDragAndDropType.SYMBOL: return localize('symbol', 'Symbol'); } } @@ -209,6 +215,11 @@ export class ChatDragAndDrop extends Themable { return []; } + if (containsDragType(e, CodeDataTransfers.SYMBOLS)) { + const data = extractSymbolDropData(e); + return this.resolveSymbolsAttachContext(data); + } + const data = extractEditorsDropData(e); return coalesce(await Promise.all(data.map(editorInput => { return this.resolveAttachContext(editorInput); @@ -245,6 +256,20 @@ export class ChatDragAndDrop extends Themable { return getResourceAttachContext(editor.resource, stat.isDirectory); } + private resolveSymbolsAttachContext(symbols: DocumentSymbolTransferData[]): IChatRequestVariableEntry[] { + return symbols.map(symbol => { + const resource = URI.file(symbol.fsPath); + return { + kind: 'symbol', + id: symbolId(resource, symbol.range), + value: { uri: resource, range: symbol.range }, + fullName: `$(${SymbolKinds.toIcon(symbol.kind).id}) ${symbol.name}`, + name: symbol.name, + isDynamic: true + }; + }); + } + private setOverlay(target: HTMLElement, type: ChatDragAndDropType | undefined): void { // Remove any previous overlay text this.overlayText?.remove(); @@ -390,3 +415,14 @@ function getImageAttachContext(editor: EditorInput | IDraggedResourceEditorInput return undefined; } + +function symbolId(resource: URI, range?: IRange): string { + let rangePart = ''; + if (range) { + rangePart = `:${range.startLineNumber}`; + if (range.startLineNumber !== range.endLineNumber) { + rangePart += `-${range.endLineNumber}`; + } + } + return resource.fsPath + rangePart; +} diff --git a/src/vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsOutline.ts b/src/vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsOutline.ts index 5154d1928f53a..17037684a4da5 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsOutline.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsOutline.ts @@ -10,7 +10,7 @@ import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } fr import { Registry } from '../../../../../platform/registry/common/platform.js'; import { LifecyclePhase } from '../../../../services/lifecycle/common/lifecycle.js'; import { IEditorPane } from '../../../../common/editor.js'; -import { DocumentSymbolComparator, DocumentSymbolAccessibilityProvider, DocumentSymbolRenderer, DocumentSymbolFilter, DocumentSymbolGroupRenderer, DocumentSymbolIdentityProvider, DocumentSymbolNavigationLabelProvider, DocumentSymbolVirtualDelegate } from './documentSymbolsTree.js'; +import { DocumentSymbolComparator, DocumentSymbolAccessibilityProvider, DocumentSymbolRenderer, DocumentSymbolFilter, DocumentSymbolGroupRenderer, DocumentSymbolIdentityProvider, DocumentSymbolNavigationLabelProvider, DocumentSymbolVirtualDelegate, DocumentSymbolDragAndDrop } from './documentSymbolsTree.js'; import { ICodeEditor, isCodeEditor, isDiffEditor } from '../../../../../editor/browser/editorBrowser.js'; import { OutlineGroup, OutlineElement, OutlineModel, TreeElement, IOutlineMarker, IOutlineModelService } from '../../../../../editor/contrib/documentSymbols/browser/outlineModel.js'; import { CancellationToken, CancellationTokenSource } from '../../../../../base/common/cancellation.js'; @@ -169,7 +169,8 @@ class DocumentSymbolsOutline implements IOutline { ? instantiationService.createInstance(DocumentSymbolFilter, 'outline') : target === OutlineTarget.Breadcrumbs ? instantiationService.createInstance(DocumentSymbolFilter, 'breadcrumbs') - : undefined + : undefined, + dnd: new DocumentSymbolDragAndDrop(), }; this.config = { diff --git a/src/vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsTree.ts b/src/vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsTree.ts index 86b72f6b03700..d54288dde4b32 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsTree.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsTree.ts @@ -8,10 +8,10 @@ import '../../../../../editor/contrib/symbolIcons/browser/symbolIcons.js'; // Th import * as dom from '../../../../../base/browser/dom.js'; import { HighlightedLabel } from '../../../../../base/browser/ui/highlightedlabel/highlightedLabel.js'; import { IIdentityProvider, IKeyboardNavigationLabelProvider, IListVirtualDelegate } from '../../../../../base/browser/ui/list/list.js'; -import { ITreeNode, ITreeRenderer, ITreeFilter } from '../../../../../base/browser/ui/tree/tree.js'; +import { ITreeNode, ITreeRenderer, ITreeFilter, ITreeDragAndDrop, ITreeDragOverReaction } from '../../../../../base/browser/ui/tree/tree.js'; import { createMatches, FuzzyScore } from '../../../../../base/common/filters.js'; import { Range } from '../../../../../editor/common/core/range.js'; -import { SymbolKind, SymbolKinds, SymbolTag, getAriaLabelForSymbol, symbolKindNames } from '../../../../../editor/common/languages.js'; +import { DocumentSymbol, SymbolKind, SymbolKinds, SymbolTag, getAriaLabelForSymbol, symbolKindNames } from '../../../../../editor/common/languages.js'; import { OutlineElement, OutlineGroup, OutlineModel } from '../../../../../editor/contrib/documentSymbols/browser/outlineModel.js'; import { localize } from '../../../../../nls.js'; import { IconLabel, IIconLabelValueOptions } from '../../../../../base/browser/ui/iconLabel/iconLabel.js'; @@ -24,6 +24,11 @@ import { IListAccessibilityProvider } from '../../../../../base/browser/ui/list/ import { IOutlineComparator, OutlineConfigKeys, OutlineTarget } from '../../../../services/outline/browser/outline.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; import { mainWindow } from '../../../../../base/browser/window.js'; +import { IDragAndDropData, DataTransfers } from '../../../../../base/browser/dnd.js'; +import { ElementsDragAndDropData } from '../../../../../base/browser/ui/list/listView.js'; +import { CodeDataTransfers } from '../../../../../platform/dnd/browser/dnd.js'; +import { withSelection } from '../../../../../platform/opener/common/opener.js'; +import { URI } from '../../../../../base/common/uri.js'; export type DocumentSymbolItem = OutlineGroup | OutlineElement; @@ -60,6 +65,66 @@ export class DocumentSymbolIdentityProvider implements IIdentityProvider { + + constructor() { } + + getDragURI(element: DocumentSymbolItem): string | null { + const resource = OutlineModel.get(element)?.uri; + if (!resource) { + return null; + } + + if (element instanceof OutlineElement) { + return symbolRangeUri(resource, element.symbol).toString(); + } else { + return resource.toString(); + } + } + + getDragLabel(elements: DocumentSymbolItem[], originalEvent: DragEvent): string | undefined { + // Multi select not supported + if (elements.length !== 1) { + return undefined; + } + + const element = elements[0]; + return element instanceof OutlineElement ? element.symbol.name : element.label; + } + + onDragStart(data: IDragAndDropData, originalEvent: DragEvent): void { + const elements = (data as ElementsDragAndDropData).elements; + const item = elements[0]; + if (!item || !originalEvent.dataTransfer) { + return; + } + + const resource = OutlineModel.get(item)?.uri; + if (!resource) { + return; + } + + const outlineElements = item instanceof OutlineElement ? [item] : Array.from(item.children.values()); + const symbolsData = outlineElements.map(oe => ({ + name: oe.symbol.name, + fsPath: resource.fsPath, + range: oe.symbol.range, + kind: oe.symbol.kind + })); + + originalEvent.dataTransfer.setData(CodeDataTransfers.SYMBOLS, JSON.stringify(symbolsData)); + originalEvent.dataTransfer.setData(DataTransfers.RESOURCES, JSON.stringify(outlineElements.map(oe => symbolRangeUri(resource, oe.symbol)))); + } + + onDragOver(): boolean | ITreeDragOverReaction { return false; } + drop(): void { } + dispose(): void { } +} + +function symbolRangeUri(resource: URI, symbol: DocumentSymbol): URI { + return withSelection(resource, symbol.range); +} + class DocumentSymbolGroupTemplate { static readonly id = 'DocumentSymbolGroupTemplate'; constructor( From c337f26917a9c83e9d1d4c380728ef20723f9f30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Moreno?= Date: Fri, 22 Nov 2024 14:16:40 +0100 Subject: [PATCH 059/119] :lipstick: (#234417) --- build/azure-pipelines/common/sign.js | 4 ++-- build/azure-pipelines/common/sign.ts | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/build/azure-pipelines/common/sign.js b/build/azure-pipelines/common/sign.js index 7b4b620d1fad8..df25de2939961 100644 --- a/build/azure-pipelines/common/sign.js +++ b/build/azure-pipelines/common/sign.js @@ -128,10 +128,10 @@ function main([esrpCliPath, type, folderPath, pattern]) { process.on('exit', () => tmp.dispose()); const key = crypto.randomBytes(32); const iv = crypto.randomBytes(16); - const encryptionDetailsPath = tmp.tmpNameSync(); - fs.writeFileSync(encryptionDetailsPath, JSON.stringify({ key: key.toString('hex'), iv: iv.toString('hex') })); const cipher = crypto.createCipheriv('aes-256-cbc', key, iv); const encryptedToken = cipher.update(process.env['SYSTEM_ACCESSTOKEN'].trim(), 'utf8', 'hex') + cipher.final('hex'); + const encryptionDetailsPath = tmp.tmpNameSync(); + fs.writeFileSync(encryptionDetailsPath, JSON.stringify({ key: key.toString('hex'), iv: iv.toString('hex') })); const encryptedTokenPath = tmp.tmpNameSync(); fs.writeFileSync(encryptedTokenPath, encryptedToken); const patternPath = tmp.tmpNameSync(); diff --git a/build/azure-pipelines/common/sign.ts b/build/azure-pipelines/common/sign.ts index df8e26ff9218c..e5f42e87da2ef 100644 --- a/build/azure-pipelines/common/sign.ts +++ b/build/azure-pipelines/common/sign.ts @@ -140,11 +140,12 @@ export function main([esrpCliPath, type, folderPath, pattern]: string[]) { const key = crypto.randomBytes(32); const iv = crypto.randomBytes(16); + const cipher = crypto.createCipheriv('aes-256-cbc', key, iv); + const encryptedToken = cipher.update(process.env['SYSTEM_ACCESSTOKEN']!.trim(), 'utf8', 'hex') + cipher.final('hex'); + const encryptionDetailsPath = tmp.tmpNameSync(); fs.writeFileSync(encryptionDetailsPath, JSON.stringify({ key: key.toString('hex'), iv: iv.toString('hex') })); - const cipher = crypto.createCipheriv('aes-256-cbc', key, iv); - const encryptedToken = cipher.update(process.env['SYSTEM_ACCESSTOKEN']!.trim(), 'utf8', 'hex') + cipher.final('hex'); const encryptedTokenPath = tmp.tmpNameSync(); fs.writeFileSync(encryptedTokenPath, encryptedToken); From 165cb5fac2ecfd6129e8a1251ddb59c273850db1 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Fri, 22 Nov 2024 14:29:39 +0100 Subject: [PATCH 060/119] :lipstick: (#234418) --- .../common/allowedExtensionsService.ts | 12 ++++++++++-- .../common/extensionManagementUtil.ts | 10 +--------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/vs/platform/extensionManagement/common/allowedExtensionsService.ts b/src/vs/platform/extensionManagement/common/allowedExtensionsService.ts index dfd7b279c35cc..452c07b3f355b 100644 --- a/src/vs/platform/extensionManagement/common/allowedExtensionsService.ts +++ b/src/vs/platform/extensionManagement/common/allowedExtensionsService.ts @@ -7,8 +7,7 @@ import { Disposable } from '../../../base/common/lifecycle.js'; import { URI } from '../../../base/common/uri.js'; import * as nls from '../../../nls.js'; import { IGalleryExtension, AllowedExtensionsConfigKey, IAllowedExtensionsService } from './extensionManagement.js'; -import { isGalleryExtension, isIExtension } from './extensionManagementUtil.js'; -import { IExtension, TargetPlatform } from '../../extensions/common/extensions.js'; +import { ExtensionType, IExtension, TargetPlatform } from '../../extensions/common/extensions.js'; import { IProductService } from '../../product/common/productService.js'; import { IMarkdownString, MarkdownString } from '../../../base/common/htmlContent.js'; import { IConfigurationService } from '../../configuration/common/configuration.js'; @@ -16,6 +15,15 @@ import { IStringDictionary } from '../../../base/common/collections.js'; import { isBoolean, isObject, isUndefined } from '../../../base/common/types.js'; import { Emitter } from '../../../base/common/event.js'; +function isGalleryExtension(extension: any): extension is IGalleryExtension { + return extension.type === 'gallery'; +} + +function isIExtension(extension: any): extension is IExtension { + return extension.type === ExtensionType.User || extension.type === ExtensionType.System; +} + + const VersionRegex = /^(?\d+\.\d+\.\d+(-.*)?)(@(?.+))?$/; type AllowedExtensionsConfigValueType = IStringDictionary; diff --git a/src/vs/platform/extensionManagement/common/extensionManagementUtil.ts b/src/vs/platform/extensionManagement/common/extensionManagementUtil.ts index 15399a1179237..9b0ef7b6ff419 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagementUtil.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagementUtil.ts @@ -5,7 +5,7 @@ import { compareIgnoreCase } from '../../../base/common/strings.js'; import { IExtensionIdentifier, IGalleryExtension, ILocalExtension, getTargetPlatform } from './extensionManagement.js'; -import { ExtensionIdentifier, ExtensionType, IExtension, TargetPlatform, UNDEFINED_PUBLISHER } from '../../extensions/common/extensions.js'; +import { ExtensionIdentifier, IExtension, TargetPlatform, UNDEFINED_PUBLISHER } from '../../extensions/common/extensions.js'; import { IFileService } from '../../files/common/files.js'; import { isLinux, platform } from '../../../base/common/platform.js'; import { URI } from '../../../base/common/uri.js'; @@ -196,11 +196,3 @@ export async function computeTargetPlatform(fileService: IFileService, logServic logService.debug('ComputeTargetPlatform:', targetPlatform); return targetPlatform; } - -export function isGalleryExtension(extension: any): extension is IGalleryExtension { - return extension.type === 'gallery'; -} - -export function isIExtension(extension: any): extension is IExtension { - return extension.type === ExtensionType.User || extension.type === ExtensionType.System; -} From 829d1a6ec7eee55a26f1b8801d0331fa559fae42 Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Fri, 22 Nov 2024 14:48:45 +0100 Subject: [PATCH 061/119] Removing the text area dom node on disposal (#234419) removing the text area dom node on disposal --- .../browser/controller/editContext/native/nativeEditContext.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vs/editor/browser/controller/editContext/native/nativeEditContext.ts b/src/vs/editor/browser/controller/editContext/native/nativeEditContext.ts index 44cdc3f03df5b..cbdafd3adaab8 100644 --- a/src/vs/editor/browser/controller/editContext/native/nativeEditContext.ts +++ b/src/vs/editor/browser/controller/editContext/native/nativeEditContext.ts @@ -167,6 +167,7 @@ export class NativeEditContext extends AbstractEditContext { // Force blue the dom node so can write in pane with no native edit context after disposal this.domNode.domNode.blur(); this.domNode.domNode.remove(); + this.textArea.domNode.remove(); super.dispose(); } From 65d6dc677a2e8a9e08fd06ef1c0eb8343174f4f5 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Fri, 22 Nov 2024 15:12:32 +0100 Subject: [PATCH 062/119] Git Blame - refactor implementation to use an additional dirty diff provider (#234420) * Initial implementation using a quick diff provider * Add proposed API to hide a dirty diff decorator --- extensions/git/src/blame.ts | 131 ++++++++---------- extensions/git/src/commands.ts | 12 -- extensions/git/src/main.ts | 7 +- extensions/git/src/repository.ts | 2 +- extensions/git/tsconfig.json | 1 + .../api/browser/mainThreadEditors.ts | 71 +++++----- .../api/browser/mainThreadQuickDiff.ts | 3 +- src/vs/workbench/api/browser/mainThreadSCM.ts | 2 + .../workbench/api/common/extHost.protocol.ts | 4 +- .../workbench/api/common/extHostQuickDiff.ts | 2 +- .../workbench/api/common/extHostTextEditor.ts | 4 +- .../api/common/extHostTextEditors.ts | 78 ++++++----- .../browser/parts/editor/editorCommands.ts | 33 ----- .../contrib/scm/browser/dirtydiffDecorator.ts | 70 +++++----- .../workbench/contrib/scm/common/quickDiff.ts | 2 + .../contrib/scm/common/quickDiffService.ts | 3 +- .../vscode.proposed.quickDiffProvider.d.ts | 1 + ...de.proposed.textEditorDiffInformation.d.ts | 4 +- 18 files changed, 192 insertions(+), 238 deletions(-) diff --git a/extensions/git/src/blame.ts b/extensions/git/src/blame.ts index 2a2c7c65396d3..3e6ad0a1752c4 100644 --- a/extensions/git/src/blame.ts +++ b/extensions/git/src/blame.ts @@ -3,12 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { DecorationOptions, l10n, Position, Range, TextEditor, TextEditorChange, TextEditorDecorationType, TextEditorChangeKind, ThemeColor, Uri, window, workspace, EventEmitter, ConfigurationChangeEvent, StatusBarItem, StatusBarAlignment, Command, MarkdownString, commands } from 'vscode'; +import { DecorationOptions, l10n, Position, Range, TextEditor, TextEditorChange, TextEditorDecorationType, TextEditorChangeKind, ThemeColor, Uri, window, workspace, EventEmitter, ConfigurationChangeEvent, StatusBarItem, StatusBarAlignment, Command, MarkdownString, QuickDiffProvider } from 'vscode'; import { Model } from './model'; import { dispose, fromNow, IDisposable, pathEquals } from './util'; import { Repository } from './repository'; import { throttle } from './decorators'; import { BlameInformation } from './git'; +import { toGitUri } from './uri'; function lineRangesContainLine(changes: readonly TextEditorChange[], lineNumber: number): boolean { return changes.some(c => c.modified.startLineNumber <= lineNumber && lineNumber < c.modified.endLineNumberExclusive); @@ -18,23 +19,6 @@ function lineRangeLength(startLineNumber: number, endLineNumberExclusive: number return endLineNumberExclusive - startLineNumber; } -function toTextEditorChange(originalStartLineNumber: number, originalEndLineNumberExclusive: number, modifiedStartLineNumber: number, modifiedEndLineNumberExclusive: number): TextEditorChange { - let kind: TextEditorChangeKind; - if (originalStartLineNumber === originalEndLineNumberExclusive) { - kind = TextEditorChangeKind.Addition; - } else if (modifiedStartLineNumber === modifiedEndLineNumberExclusive) { - kind = TextEditorChangeKind.Deletion; - } else { - kind = TextEditorChangeKind.Modification; - } - - return { - original: { startLineNumber: originalStartLineNumber, endLineNumberExclusive: originalEndLineNumberExclusive }, - modified: { startLineNumber: modifiedStartLineNumber, endLineNumberExclusive: modifiedEndLineNumberExclusive }, - kind - }; -} - function mapModifiedLineNumberToOriginalLineNumber(lineNumber: number, changes: readonly TextEditorChange[]): number { if (changes.length === 0) { return lineNumber; @@ -79,14 +63,13 @@ interface LineBlameInformation { readonly blameInformation: BlameInformation | string; } -export class GitBlameController { +export class GitBlameController implements QuickDiffProvider { private readonly _onDidChangeBlameInformation = new EventEmitter(); public readonly onDidChangeBlameInformation = this._onDidChangeBlameInformation.event; readonly textEditorBlameInformation = new Map(); private readonly _repositoryBlameInformation = new Map(); - private readonly _stagedResourceDiffInformation = new Map>(); private _repositoryDisposables = new Map(); private _disposables: IDisposable[] = []; @@ -104,6 +87,25 @@ export class GitBlameController { this._updateTextEditorBlameInformation(window.activeTextEditor); } + get visible(): boolean { + return false; + } + + provideOriginalResource(uri: Uri): Uri | undefined { + // Ignore resources outside a repository + const repository = this._model.getRepository(uri); + if (!repository) { + return undefined; + } + + // Ignore resources that are not in the index group + if (!repository.indexGroup.resourceStates.some(r => pathEquals(r.resourceUri.fsPath, uri.fsPath))) { + return undefined; + } + + return toGitUri(uri, 'HEAD', { replaceFileExtension: true }); + } + getBlameInformationHover(documentUri: Uri, blameInformation: BlameInformation | string): MarkdownString { if (typeof blameInformation === 'string') { return new MarkdownString(blameInformation, true); @@ -141,9 +143,7 @@ export class GitBlameController { private _onDidOpenRepository(repository: Repository): void { const repositoryDisposables: IDisposable[] = []; - repository.onDidRunGitStatus(() => this._onDidRunGitStatus(repository), this, repositoryDisposables); - repository.onDidChangeRepository(e => this._onDidChangeRepository(repository, e), this, this._disposables); this._repositoryDisposables.set(repository, repositoryDisposables); } @@ -174,14 +174,6 @@ export class GitBlameController { } } - private _onDidChangeRepository(repository: Repository, uri: Uri): void { - if (!/\.git\/index$/.test(uri.fsPath)) { - return; - } - - this._stagedResourceDiffInformation.delete(repository); - } - private async _getBlameInformation(resource: Uri): Promise { const repository = this._model.getRepository(resource); if (!repository || !repository.HEAD?.commit) { @@ -209,76 +201,65 @@ export class GitBlameController { return resourceBlameInformation; } - private async _getStagedResourceDiffInformation(uri: Uri): Promise { - const repository = this._model.getRepository(uri); - if (!repository) { - return undefined; + @throttle + private async _updateTextEditorBlameInformation(textEditor: TextEditor | undefined): Promise { + if (!textEditor?.diffInformation) { + return; } - const [resource] = repository.indexGroup - .resourceStates.filter(r => pathEquals(uri.fsPath, r.resourceUri.fsPath)); - - if (!resource || !resource.leftUri || !resource.rightUri) { - return undefined; - } + // Working tree diff information + const diffInformationWorkingTree = textEditor.diffInformation + .filter(diff => diff.original?.scheme === 'git') + .find(diff => { + const query = JSON.parse(diff.original!.query) as { ref: string }; + return query.ref !== 'HEAD'; + }); - const diffInformationMap = this._stagedResourceDiffInformation.get(repository) ?? new Map(); - let changes = diffInformationMap.get(resource.resourceUri); - if (changes) { - return changes; - } + // Working tree + index diff information + const diffInformationWorkingTreeAndIndex = textEditor.diffInformation + .filter(diff => diff.original?.scheme === 'git') + .find(diff => { + const query = JSON.parse(diff.original!.query) as { ref: string }; + return query.ref === 'HEAD'; + }); - // Get the diff information for the staged resource - const diffInformation: [number, number, number, number][] = await commands.executeCommand('_workbench.internal.computeDiff', resource.leftUri, resource.rightUri); - if (!diffInformation) { - return undefined; + // Working tree diff information is not present or it is stale + if (!diffInformationWorkingTree || diffInformationWorkingTree.isStale) { + return; } - changes = diffInformation.map(change => toTextEditorChange(change[0], change[1], change[2], change[3])); - this._stagedResourceDiffInformation.set(repository, diffInformationMap.set(resource.resourceUri, changes)); - - return changes; - } - - @throttle - private async _updateTextEditorBlameInformation(textEditor: TextEditor | undefined): Promise { - const diffInformation = textEditor?.diffInformation; - if (!diffInformation || diffInformation.isStale) { + // Working tree + index diff information is present and it is stale + if (diffInformationWorkingTreeAndIndex && diffInformationWorkingTreeAndIndex.isStale) { return; } + // For staged resources, we provide an additional "original resource" so that core can + // compute the diff information that contains the changes from the working tree and the + // index. + const diffInformation = diffInformationWorkingTreeAndIndex ?? diffInformationWorkingTree; + + // Git blame information const resourceBlameInformation = await this._getBlameInformation(textEditor.document.uri); if (!resourceBlameInformation) { return; } - // The diff information does not contain changes that have been staged. We need - // to get the staged changes and if present, merge them with the diff information. - const diffInformationStagedResources: TextEditorChange[] = await this._getStagedResourceDiffInformation(textEditor.document.uri) ?? []; - - console.log('diffInformation', diffInformation.changes); - console.log('diffInformationStagedResources', diffInformationStagedResources); - - console.log('resourceBlameInformation', resourceBlameInformation); - const lineBlameInformation: LineBlameInformation[] = []; for (const lineNumber of textEditor.selections.map(s => s.active.line)) { - // Check if the line is contained in the diff information - if (lineRangesContainLine(diffInformation.changes, lineNumber + 1)) { + // Check if the line is contained in the working tree diff information + if (lineRangesContainLine(diffInformationWorkingTree.changes, lineNumber + 1)) { lineBlameInformation.push({ lineNumber, blameInformation: l10n.t('Not Committed Yet') }); continue; } - // Check if the line is contained in the staged resources diff information - if (lineRangesContainLine(diffInformationStagedResources, lineNumber + 1)) { + // Check if the line is contained in the working tree + index diff information + if (lineRangesContainLine(diffInformationWorkingTreeAndIndex?.changes ?? [], lineNumber + 1)) { lineBlameInformation.push({ lineNumber, blameInformation: l10n.t('Not Committed Yet (Staged)') }); continue; } - const diffInformationAll = [...diffInformation.changes, ...diffInformationStagedResources]; - // Map the line number to the git blame ranges using the diff information - const lineNumberWithDiff = mapModifiedLineNumberToOriginalLineNumber(lineNumber + 1, diffInformationAll); + const lineNumberWithDiff = mapModifiedLineNumberToOriginalLineNumber(lineNumber + 1, diffInformation.changes); const blameInformation = resourceBlameInformation.find(blameInformation => { return blameInformation.ranges.find(range => { return lineNumberWithDiff >= range.startLineNumber && lineNumberWithDiff <= range.endLineNumber; diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index 43518a7784393..192f893d3aa5b 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -1559,10 +1559,6 @@ export class CommandCenter { async stageSelectedChanges(changes: LineChange[]): Promise { const textEditor = window.activeTextEditor; - this.logger.debug('[CommandCenter][stageSelectedChanges] changes:', changes); - this.logger.debug('[CommandCenter][stageSelectedChanges] diffInformation.changes:', textEditor?.diffInformation?.changes); - this.logger.debug('[CommandCenter][stageSelectedChanges] diffInformation.isStale:', textEditor?.diffInformation?.isStale); - if (!textEditor) { return; } @@ -1745,10 +1741,6 @@ export class CommandCenter { async revertSelectedRanges(changes: LineChange[]): Promise { const textEditor = window.activeTextEditor; - this.logger.debug('[CommandCenter][revertSelectedRanges] changes:', changes); - this.logger.debug('[CommandCenter][revertSelectedRanges] diffInformation.changes:', textEditor?.diffInformation?.changes); - this.logger.debug('[CommandCenter][revertSelectedRanges] diffInformation.isStale:', textEditor?.diffInformation?.isStale); - if (!textEditor) { return; } @@ -1826,10 +1818,6 @@ export class CommandCenter { async unstageSelectedRanges(changes: LineChange[]): Promise { const textEditor = window.activeTextEditor; - this.logger.debug('[CommandCenter][unstageSelectedRanges] changes:', changes); - this.logger.debug('[CommandCenter][unstageSelectedRanges] diffInformation.changes:', textEditor?.diffInformation?.changes); - this.logger.debug('[CommandCenter][unstageSelectedRanges] diffInformation.isStale:', textEditor?.diffInformation?.isStale); - if (!textEditor) { return; } diff --git a/extensions/git/src/main.ts b/extensions/git/src/main.ts index 17517d75c5619..da5452bf1c367 100644 --- a/extensions/git/src/main.ts +++ b/extensions/git/src/main.ts @@ -113,12 +113,17 @@ async function createModel(context: ExtensionContext, logger: LogOutputChannel, cc, new GitFileSystemProvider(model), new GitDecorations(model), - new GitBlameController(model), new GitTimelineProvider(model, cc), new GitEditSessionIdentityProvider(model), new TerminalShellExecutionManager(model, logger) ); + const blameController = new GitBlameController(model); + disposables.push(blameController); + + const quickDiffProvider = window.registerQuickDiffProvider({ scheme: 'file' }, blameController, 'Git local changes (working tree + index)'); + disposables.push(quickDiffProvider); + const postCommitCommandsProvider = new GitPostCommitCommandsProvider(); model.registerPostCommitCommandsProvider(postCommitCommandsProvider); diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index 89f3221b6fb4b..795b9f66f0248 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -1021,7 +1021,7 @@ export class Repository implements Disposable { * Quick diff label */ get label(): string { - return l10n.t('Git local working changes'); + return l10n.t('Git local changes (working tree)'); } provideOriginalResource(uri: Uri): Uri | undefined { diff --git a/extensions/git/tsconfig.json b/extensions/git/tsconfig.json index 82ba0abe5463a..ccf029e2df377 100644 --- a/extensions/git/tsconfig.json +++ b/extensions/git/tsconfig.json @@ -11,6 +11,7 @@ "src/**/*", "../../src/vscode-dts/vscode.d.ts", "../../src/vscode-dts/vscode.proposed.diffCommand.d.ts", + "../../src/vscode-dts/vscode.proposed.quickDiffProvider.d.ts", "../../src/vscode-dts/vscode.proposed.quickPickSortByLabel.d.ts", "../../src/vscode-dts/vscode.proposed.scmActionButton.d.ts", "../../src/vscode-dts/vscode.proposed.scmHistoryProvider.d.ts", diff --git a/src/vs/workbench/api/browser/mainThreadEditors.ts b/src/vs/workbench/api/browser/mainThreadEditors.ts index 93b9331889d18..611a260c9eed9 100644 --- a/src/vs/workbench/api/browser/mainThreadEditors.ts +++ b/src/vs/workbench/api/browser/mainThreadEditors.ts @@ -34,6 +34,7 @@ import { autorun, constObservable, derived, derivedOpts, IObservable, observable import { IUriIdentityService } from '../../../platform/uriIdentity/common/uriIdentity.js'; import { isITextModel } from '../../../editor/common/model.js'; import { LineRangeMapping, lineRangeMappingFromChanges } from '../../../editor/common/diff/rangeMapping.js'; +import { equals } from '../../../base/common/arrays.js'; export interface IMainThreadEditorLocator { getEditor(id: string): MainThreadTextEditor | undefined; @@ -129,7 +130,7 @@ export class MainThreadTextEditors implements MainThreadTextEditorsShape { return result; } - private _getTextEditorDiffInformation(textEditor: MainThreadTextEditor): IObservable { + private _getTextEditorDiffInformation(textEditor: MainThreadTextEditor): IObservable { const codeEditor = textEditor.getCodeEditor(); if (!codeEditor) { return constObservable(undefined); @@ -145,7 +146,7 @@ export class MainThreadTextEditors implements MainThreadTextEditorsShape { ? observableFromEvent(this, diffEditor.onDidChangeModel, () => diffEditor.getModel()) : observableFromEvent(this, codeEditor.onDidChangeModel, () => codeEditor.getModel()); - const editorChangesObs = derived>(reader => { + const editorChangesObs = derived>(reader => { const editorModel = editorModelObs.read(reader); if (!editorModel) { return constObservable(undefined); @@ -156,11 +157,11 @@ export class MainThreadTextEditors implements MainThreadTextEditorsShape { return observableFromEvent(diffEditor.onDidUpdateDiff, () => { const changes = diffEditor.getDiffComputationResult()?.changes2 ?? []; - return { + return [{ original: editorModel.original.uri, modified: editorModel.modified.uri, - changes: changes.map(change => change as LineRangeMapping) - }; + lineRangeMappings: changes.map(change => change as LineRangeMapping) + }]; }); } @@ -171,29 +172,25 @@ export class MainThreadTextEditors implements MainThreadTextEditorsShape { } return observableFromEvent(this, dirtyDiffModel.onDidChange, () => { - const scmQuickDiff = dirtyDiffModel.quickDiffs.find(diff => diff.isSCM === true); - if (!scmQuickDiff) { - return undefined; - } - - const changes = dirtyDiffModel.changes - .filter(change => change.label === scmQuickDiff.label) - .map(change => change.change); + return dirtyDiffModel.quickDiffs.map(quickDiff => { + const changes = dirtyDiffModel.changes + .filter(change => change.label === quickDiff.label) + .map(change => change.change); - // Convert IChange[] to LineRangeMapping[] - const lineRangeMapping = lineRangeMappingFromChanges(changes); - - return { - original: scmQuickDiff.originalResource, - modified: editorModel.uri, - changes: lineRangeMapping - }; + // Convert IChange[] to LineRangeMapping[] + const lineRangeMappings = lineRangeMappingFromChanges(changes); + return { + original: quickDiff.originalResource, + modified: editorModel.uri, + lineRangeMappings + }; + }); }); }); return derivedOpts({ owner: this, - equalsFn: (diff1, diff2) => isTextEditorDiffInformationEqual(this._uriIdentityService, diff1, diff2) + equalsFn: (diff1, diff2) => equals(diff1, diff2, (a, b) => isTextEditorDiffInformationEqual(this._uriIdentityService, a, b)) }, reader => { const editorModel = editorModelObs.read(reader); const editorChanges = editorChangesObs.read(reader).read(reader); @@ -205,20 +202,22 @@ export class MainThreadTextEditors implements MainThreadTextEditorsShape { ? editorModel.getVersionId() : editorModel.modified.getVersionId(); - const changes: ITextEditorChange[] = editorChanges.changes - .map(change => [ - change.original.startLineNumber, - change.original.endLineNumberExclusive, - change.modified.startLineNumber, - change.modified.endLineNumberExclusive - ]); - - return { - documentVersion, - original: editorChanges.original, - modified: editorChanges.modified, - changes - }; + return editorChanges.map(change => { + const changes: ITextEditorChange[] = change.lineRangeMappings + .map(change => [ + change.original.startLineNumber, + change.original.endLineNumberExclusive, + change.modified.startLineNumber, + change.modified.endLineNumberExclusive + ]); + + return { + documentVersion, + original: change.original, + modified: change.modified, + changes + }; + }); }); } diff --git a/src/vs/workbench/api/browser/mainThreadQuickDiff.ts b/src/vs/workbench/api/browser/mainThreadQuickDiff.ts index 541de6b2b5dc8..d86233aabf9dc 100644 --- a/src/vs/workbench/api/browser/mainThreadQuickDiff.ts +++ b/src/vs/workbench/api/browser/mainThreadQuickDiff.ts @@ -23,12 +23,13 @@ export class MainThreadQuickDiff implements MainThreadQuickDiffShape { this.proxy = extHostContext.getProxy(ExtHostContext.ExtHostQuickDiff); } - async $registerQuickDiffProvider(handle: number, selector: IDocumentFilterDto[], label: string, rootUri: UriComponents | undefined): Promise { + async $registerQuickDiffProvider(handle: number, selector: IDocumentFilterDto[], label: string, rootUri: UriComponents | undefined, visible: boolean): Promise { const provider: QuickDiffProvider = { label, rootUri: URI.revive(rootUri), selector, isSCM: false, + visible, getOriginalResource: async (uri: URI) => { return URI.revive(await this.proxy.$provideOriginalResource(handle, uri, CancellationToken.None)); } diff --git a/src/vs/workbench/api/browser/mainThreadSCM.ts b/src/vs/workbench/api/browser/mainThreadSCM.ts index fbb4340c5ec62..a916634b8ceba 100644 --- a/src/vs/workbench/api/browser/mainThreadSCM.ts +++ b/src/vs/workbench/api/browser/mainThreadSCM.ts @@ -289,6 +289,7 @@ class MainThreadSCMProvider implements ISCMProvider, QuickDiffProvider { private _quickDiff: IDisposable | undefined; public readonly isSCM: boolean = true; + public readonly visible: boolean = true; private readonly _historyProvider = observableValue(this, undefined); get historyProvider() { return this._historyProvider; } @@ -338,6 +339,7 @@ class MainThreadSCMProvider implements ISCMProvider, QuickDiffProvider { label: features.quickDiffLabel ?? this.label, rootUri: this.rootUri, isSCM: this.isSCM, + visible: this.visible, getOriginalResource: (uri: URI) => this.getOriginalResource(uri) }); } else if (features.hasQuickDiffProvider === false && this._quickDiff) { diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 4fb99e4aed94f..ae5441d23a753 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -1646,7 +1646,7 @@ export interface MainThreadSCMShape extends IDisposable { } export interface MainThreadQuickDiffShape extends IDisposable { - $registerQuickDiffProvider(handle: number, selector: IDocumentFilterDto[], label: string, rootUri: UriComponents | undefined): Promise; + $registerQuickDiffProvider(handle: number, selector: IDocumentFilterDto[], label: string, rootUri: UriComponents | undefined, visible: boolean): Promise; $unregisterQuickDiffProvider(handle: number): Promise; } @@ -1848,7 +1848,7 @@ export interface ISelectionChangeEvent { export interface ExtHostEditorsShape { $acceptEditorPropertiesChanged(id: string, props: IEditorPropertiesChangeData): void; $acceptEditorPositionData(data: ITextEditorPositionData): void; - $acceptEditorDiffInformation(id: string, diffInformation: ITextEditorDiffInformation | undefined): void; + $acceptEditorDiffInformation(id: string, diffInformation: ITextEditorDiffInformation[] | undefined): void; } export interface IDocumentsAndEditorsDelta { diff --git a/src/vs/workbench/api/common/extHostQuickDiff.ts b/src/vs/workbench/api/common/extHostQuickDiff.ts index f959ebf9d92b2..2b04fcd6222f7 100644 --- a/src/vs/workbench/api/common/extHostQuickDiff.ts +++ b/src/vs/workbench/api/common/extHostQuickDiff.ts @@ -39,7 +39,7 @@ export class ExtHostQuickDiff implements ExtHostQuickDiffShape { registerQuickDiffProvider(selector: vscode.DocumentSelector, quickDiffProvider: vscode.QuickDiffProvider, label: string, rootUri?: vscode.Uri): vscode.Disposable { const handle = ExtHostQuickDiff.handlePool++; this.providers.set(handle, quickDiffProvider); - this.proxy.$registerQuickDiffProvider(handle, DocumentSelector.from(selector, this.uriTransformer), label, rootUri); + this.proxy.$registerQuickDiffProvider(handle, DocumentSelector.from(selector, this.uriTransformer), label, rootUri, quickDiffProvider.visible ?? true); return { dispose: () => { this.proxy.$unregisterQuickDiffProvider(handle); diff --git a/src/vs/workbench/api/common/extHostTextEditor.ts b/src/vs/workbench/api/common/extHostTextEditor.ts index df874f990a334..0803130ffbd9a 100644 --- a/src/vs/workbench/api/common/extHostTextEditor.ts +++ b/src/vs/workbench/api/common/extHostTextEditor.ts @@ -412,7 +412,7 @@ export class ExtHostTextEditor { private _viewColumn: vscode.ViewColumn | undefined; private _disposed: boolean = false; private _hasDecorationsForKey = new Set(); - private _diffInformation: vscode.TextEditorDiffInformation | undefined; + private _diffInformation: vscode.TextEditorDiffInformation[] | undefined; readonly value: vscode.TextEditor; @@ -604,7 +604,7 @@ export class ExtHostTextEditor { this._selections = selections; } - _acceptDiffInformation(diffInformation: vscode.TextEditorDiffInformation | undefined): void { + _acceptDiffInformation(diffInformation: vscode.TextEditorDiffInformation[] | undefined): void { ok(!this._disposed); this._diffInformation = diffInformation; } diff --git a/src/vs/workbench/api/common/extHostTextEditors.ts b/src/vs/workbench/api/common/extHostTextEditors.ts index 7764ac504c239..7655c14f8d7d1 100644 --- a/src/vs/workbench/api/common/extHostTextEditors.ts +++ b/src/vs/workbench/api/common/extHostTextEditors.ts @@ -160,7 +160,7 @@ export class ExtHostEditors extends Disposable implements ExtHostEditorsShape { } } - $acceptEditorDiffInformation(id: string, diffInformation: ITextEditorDiffInformation | undefined): void { + $acceptEditorDiffInformation(id: string, diffInformation: ITextEditorDiffInformation[] | undefined): void { const textEditor = this._extHostDocumentsAndEditors.getEditor(id); if (!textEditor) { throw new Error('unknown text editor'); @@ -175,44 +175,46 @@ export class ExtHostEditors extends Disposable implements ExtHostEditorsShape { return; } - const original = URI.revive(diffInformation.original); - const modified = URI.revive(diffInformation.modified); - - const changes = diffInformation.changes.map(change => { - const [originalStartLineNumber, originalEndLineNumberExclusive, modifiedStartLineNumber, modifiedEndLineNumberExclusive] = change; - - let kind: vscode.TextEditorChangeKind; - if (originalStartLineNumber === originalEndLineNumberExclusive) { - kind = TextEditorChangeKind.Addition; - } else if (modifiedStartLineNumber === modifiedEndLineNumberExclusive) { - kind = TextEditorChangeKind.Deletion; - } else { - kind = TextEditorChangeKind.Modification; - } - - return { - original: { - startLineNumber: originalStartLineNumber, - endLineNumberExclusive: originalEndLineNumberExclusive - }, - modified: { - startLineNumber: modifiedStartLineNumber, - endLineNumberExclusive: modifiedEndLineNumberExclusive - }, - kind - } satisfies vscode.TextEditorChange; - }); - const that = this; - const result = Object.freeze({ - documentVersion: diffInformation.documentVersion, - original, - modified, - changes, - get isStale(): boolean { - const document = that._extHostDocumentsAndEditors.getDocument(modified); - return document?.version !== diffInformation.documentVersion; - } + const result = diffInformation.map(diff => { + const original = URI.revive(diff.original); + const modified = URI.revive(diff.modified); + + const changes = diff.changes.map(change => { + const [originalStartLineNumber, originalEndLineNumberExclusive, modifiedStartLineNumber, modifiedEndLineNumberExclusive] = change; + + let kind: vscode.TextEditorChangeKind; + if (originalStartLineNumber === originalEndLineNumberExclusive) { + kind = TextEditorChangeKind.Addition; + } else if (modifiedStartLineNumber === modifiedEndLineNumberExclusive) { + kind = TextEditorChangeKind.Deletion; + } else { + kind = TextEditorChangeKind.Modification; + } + + return { + original: { + startLineNumber: originalStartLineNumber, + endLineNumberExclusive: originalEndLineNumberExclusive + }, + modified: { + startLineNumber: modifiedStartLineNumber, + endLineNumberExclusive: modifiedEndLineNumberExclusive + }, + kind + } satisfies vscode.TextEditorChange; + }); + + return Object.freeze({ + documentVersion: diff.documentVersion, + original, + modified, + changes, + get isStale(): boolean { + const document = that._extHostDocumentsAndEditors.getDocument(modified); + return document?.version !== diff.documentVersion; + } + }); }); textEditor._acceptDiffInformation(result); diff --git a/src/vs/workbench/browser/parts/editor/editorCommands.ts b/src/vs/workbench/browser/parts/editor/editorCommands.ts index b22b9bc033aad..1cecdaab32f09 100644 --- a/src/vs/workbench/browser/parts/editor/editorCommands.ts +++ b/src/vs/workbench/browser/parts/editor/editorCommands.ts @@ -40,8 +40,6 @@ import { IPathService } from '../../../services/path/common/pathService.js'; import { IUntitledTextEditorService } from '../../../services/untitled/common/untitledTextEditorService.js'; import { DIFF_FOCUS_OTHER_SIDE, DIFF_FOCUS_PRIMARY_SIDE, DIFF_FOCUS_SECONDARY_SIDE, DIFF_OPEN_SIDE, registerDiffEditorCommands } from './diffEditorCommands.js'; import { IResolvedEditorCommandsContext, resolveCommandsContext } from './editorCommandsContext.js'; -import { IEditorWorkerService } from '../../../../editor/common/services/editorWorker.js'; -import { ITextModelService } from '../../../../editor/common/services/resolverService.js'; export const CLOSE_SAVED_EDITORS_COMMAND_ID = 'workbench.action.closeUnmodifiedEditors'; export const CLOSE_EDITORS_IN_GROUP_COMMAND_ID = 'workbench.action.closeEditorsInGroup'; @@ -538,37 +536,6 @@ function registerOpenEditorAPICommands(): void { label: options.title, }); }); - - CommandsRegistry.registerCommand('_workbench.internal.computeDiff', async (accessor: ServicesAccessor, original: UriComponents, modified: UriComponents) => { - const configurationService = accessor.get(IConfigurationService); - const editorWorkerService = accessor.get(IEditorWorkerService); - const textModelService = accessor.get(ITextModelService); - - const originalResource = URI.revive(original); - const modifiedResource = URI.revive(modified); - - const originalTextModel = await textModelService.createModelReference(originalResource); - const modifiedTextModel = await textModelService.createModelReference(modifiedResource); - - const ignoreTrimWhitespaceSetting = configurationService.getValue<'true' | 'false' | 'inherit'>('scm.diffDecorationsIgnoreTrimWhitespace'); - const ignoreTrimWhitespace = ignoreTrimWhitespaceSetting === 'inherit' - ? configurationService.getValue('diffEditor.ignoreTrimWhitespace') - : ignoreTrimWhitespaceSetting !== 'false'; - - const changes = await editorWorkerService.computeDiff(originalResource, modifiedResource, { - computeMoves: false, - ignoreTrimWhitespace, - maxComputationTimeMs: Number.MAX_SAFE_INTEGER - }, 'legacy'); - - originalTextModel.dispose(); - modifiedTextModel.dispose(); - - return changes?.changes.map(c => [ - c.original.startLineNumber, c.original.endLineNumberExclusive, - c.modified.startLineNumber, c.modified.endLineNumberExclusive - ]); - }); } interface OpenMultiFileDiffEditorOptions { diff --git a/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts b/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts index 8479aa0c44477..99235d9416500 100644 --- a/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts +++ b/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts @@ -1121,40 +1121,44 @@ class DirtyDiffDecorator extends Disposable { return; } + const visibleQuickDiffs = this.model.quickDiffs.filter(quickDiff => quickDiff.visible); const pattern = this.configurationService.getValue<{ added: boolean; modified: boolean }>('scm.diffDecorationsGutterPattern'); - const decorations = this.model.changes.map((labeledChange) => { - const change = labeledChange.change; - const changeType = getChangeType(change); - const startLineNumber = change.modifiedStartLineNumber; - const endLineNumber = change.modifiedEndLineNumber || startLineNumber; - - switch (changeType) { - case ChangeType.Add: - return { - range: { - startLineNumber: startLineNumber, startColumn: 1, - endLineNumber: endLineNumber, endColumn: 1 - }, - options: pattern.added ? this.addedPatternOptions : this.addedOptions - }; - case ChangeType.Delete: - return { - range: { - startLineNumber: startLineNumber, startColumn: Number.MAX_VALUE, - endLineNumber: startLineNumber, endColumn: Number.MAX_VALUE - }, - options: this.deletedOptions - }; - case ChangeType.Modify: - return { - range: { - startLineNumber: startLineNumber, startColumn: 1, - endLineNumber: endLineNumber, endColumn: 1 - }, - options: pattern.modified ? this.modifiedPatternOptions : this.modifiedOptions - }; - } - }); + + const decorations = this.model.changes + .filter(labeledChange => visibleQuickDiffs.some(quickDiff => quickDiff.label === labeledChange.label)) + .map((labeledChange) => { + const change = labeledChange.change; + const changeType = getChangeType(change); + const startLineNumber = change.modifiedStartLineNumber; + const endLineNumber = change.modifiedEndLineNumber || startLineNumber; + + switch (changeType) { + case ChangeType.Add: + return { + range: { + startLineNumber: startLineNumber, startColumn: 1, + endLineNumber: endLineNumber, endColumn: 1 + }, + options: pattern.added ? this.addedPatternOptions : this.addedOptions + }; + case ChangeType.Delete: + return { + range: { + startLineNumber: startLineNumber, startColumn: Number.MAX_VALUE, + endLineNumber: startLineNumber, endColumn: Number.MAX_VALUE + }, + options: this.deletedOptions + }; + case ChangeType.Modify: + return { + range: { + startLineNumber: startLineNumber, startColumn: 1, + endLineNumber: endLineNumber, endColumn: 1 + }, + options: pattern.modified ? this.modifiedPatternOptions : this.modifiedOptions + }; + } + }); if (!this.decorationsCollection) { this.decorationsCollection = this.codeEditor.createDecorationsCollection(decorations); diff --git a/src/vs/workbench/contrib/scm/common/quickDiff.ts b/src/vs/workbench/contrib/scm/common/quickDiff.ts index 65142c9a4c800..7b1d5daf31864 100644 --- a/src/vs/workbench/contrib/scm/common/quickDiff.ts +++ b/src/vs/workbench/contrib/scm/common/quickDiff.ts @@ -16,6 +16,7 @@ export interface QuickDiffProvider { rootUri: URI | undefined; selector?: LanguageSelector; isSCM: boolean; + visible: boolean; getOriginalResource(uri: URI): Promise; } @@ -23,6 +24,7 @@ export interface QuickDiff { label: string; originalResource: URI; isSCM: boolean; + visible: boolean; } export interface IQuickDiffService { diff --git a/src/vs/workbench/contrib/scm/common/quickDiffService.ts b/src/vs/workbench/contrib/scm/common/quickDiffService.ts index 99bcd00d8c80a..38a9d67a55508 100644 --- a/src/vs/workbench/contrib/scm/common/quickDiffService.ts +++ b/src/vs/workbench/contrib/scm/common/quickDiffService.ts @@ -72,7 +72,8 @@ export class QuickDiffService extends Disposable implements IQuickDiffService { const diff: Partial = { originalResource: scoreValue > 0 ? await provider.getOriginalResource(uri) ?? undefined : undefined, label: provider.label, - isSCM: provider.isSCM + isSCM: provider.isSCM, + visible: provider.visible }; return diff; })); diff --git a/src/vscode-dts/vscode.proposed.quickDiffProvider.d.ts b/src/vscode-dts/vscode.proposed.quickDiffProvider.d.ts index 3778e7092eb67..923a98917f6aa 100644 --- a/src/vscode-dts/vscode.proposed.quickDiffProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.quickDiffProvider.d.ts @@ -13,5 +13,6 @@ declare module 'vscode' { interface QuickDiffProvider { label?: string; + readonly visible?: boolean; } } diff --git a/src/vscode-dts/vscode.proposed.textEditorDiffInformation.d.ts b/src/vscode-dts/vscode.proposed.textEditorDiffInformation.d.ts index d737a5c2ef144..b86d377c76601 100644 --- a/src/vscode-dts/vscode.proposed.textEditorDiffInformation.d.ts +++ b/src/vscode-dts/vscode.proposed.textEditorDiffInformation.d.ts @@ -34,11 +34,11 @@ declare module 'vscode' { export interface TextEditorDiffInformationChangeEvent { readonly textEditor: TextEditor; - readonly diffInformation: TextEditorDiffInformation | undefined; + readonly diffInformation: TextEditorDiffInformation[] | undefined; } export interface TextEditor { - readonly diffInformation: TextEditorDiffInformation | undefined; + readonly diffInformation: TextEditorDiffInformation[] | undefined; } export namespace window { From 22dd305e92f869435be1ee8a36421c1911563b58 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Fri, 22 Nov 2024 14:36:09 +0000 Subject: [PATCH 063/119] renderer side inline hint and cmd to show/hide (#234421) --- .../browser/inlineChat.contribution.ts | 6 +- .../inlineChat/browser/inlineChatActions.ts | 4 +- .../browser/inlineChatCurrentLine.ts | 289 ++++++++++++------ .../inlineChat/browser/media/inlineChat.css | 11 + .../contrib/inlineChat/common/inlineChat.ts | 1 + 5 files changed, 207 insertions(+), 104 deletions(-) diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts index 2cc76db29aa94..64d4f2a11d9c5 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts @@ -24,7 +24,7 @@ import { localize } from '../../../../nls.js'; import { ChatContextKeys } from '../../chat/common/chatContextKeys.js'; import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; import { InlineChatAccessibilityHelp } from './inlineChatAccessibilityHelp.js'; -import { InlineChatExansionContextKey, InlineChatExpandLineAction } from './inlineChatCurrentLine.js'; +import { InlineChatExpandLineAction, InlineChatHintsController, HideInlineChatHintAction, ShowInlineChatHintAction } from './inlineChatCurrentLine.js'; // --- browser @@ -34,8 +34,10 @@ registerSingleton(IInlineChatSavingService, InlineChatSavingServiceImpl, Instant registerEditorContribution(INLINE_CHAT_ID, InlineChatController, EditorContributionInstantiation.Eager); // EAGER because of notebook dispose/create of editors -registerEditorContribution(InlineChatExansionContextKey.Id, InlineChatExansionContextKey, EditorContributionInstantiation.BeforeFirstInteraction); registerAction2(InlineChatExpandLineAction); +registerAction2(ShowInlineChatHintAction); +registerAction2(HideInlineChatHintAction); +registerEditorContribution(InlineChatHintsController.ID, InlineChatHintsController, EditorContributionInstantiation.Lazy); // --- MENU special --- diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts index 84f00a76493a5..b101d5c1d59ed 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts @@ -11,7 +11,7 @@ import { EmbeddedDiffEditorWidget } from '../../../../editor/browser/widget/diff import { EmbeddedCodeEditorWidget } from '../../../../editor/browser/widget/codeEditor/embeddedCodeEditorWidget.js'; import { EditorContextKeys } from '../../../../editor/common/editorContextKeys.js'; import { InlineChatController, InlineChatRunOptions } from './inlineChatController.js'; -import { ACTION_ACCEPT_CHANGES, CTX_INLINE_CHAT_HAS_AGENT, CTX_INLINE_CHAT_HAS_STASHED_SESSION, CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_INNER_CURSOR_FIRST, CTX_INLINE_CHAT_INNER_CURSOR_LAST, CTX_INLINE_CHAT_VISIBLE, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, CTX_INLINE_CHAT_DOCUMENT_CHANGED, CTX_INLINE_CHAT_EDIT_MODE, EditMode, MENU_INLINE_CHAT_WIDGET_STATUS, CTX_INLINE_CHAT_REQUEST_IN_PROGRESS, CTX_INLINE_CHAT_RESPONSE_TYPE, InlineChatResponseType, ACTION_REGENERATE_RESPONSE, ACTION_VIEW_IN_CHAT, ACTION_TOGGLE_DIFF, CTX_INLINE_CHAT_CHANGE_HAS_DIFF, CTX_INLINE_CHAT_CHANGE_SHOWS_DIFF, MENU_INLINE_CHAT_ZONE, ACTION_DISCARD_CHANGES, CTX_INLINE_CHAT_POSSIBLE } from '../common/inlineChat.js'; +import { ACTION_ACCEPT_CHANGES, CTX_INLINE_CHAT_HAS_AGENT, CTX_INLINE_CHAT_HAS_STASHED_SESSION, CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_INNER_CURSOR_FIRST, CTX_INLINE_CHAT_INNER_CURSOR_LAST, CTX_INLINE_CHAT_VISIBLE, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, CTX_INLINE_CHAT_DOCUMENT_CHANGED, CTX_INLINE_CHAT_EDIT_MODE, EditMode, MENU_INLINE_CHAT_WIDGET_STATUS, CTX_INLINE_CHAT_REQUEST_IN_PROGRESS, CTX_INLINE_CHAT_RESPONSE_TYPE, InlineChatResponseType, ACTION_REGENERATE_RESPONSE, ACTION_VIEW_IN_CHAT, ACTION_TOGGLE_DIFF, CTX_INLINE_CHAT_CHANGE_HAS_DIFF, CTX_INLINE_CHAT_CHANGE_SHOWS_DIFF, MENU_INLINE_CHAT_ZONE, ACTION_DISCARD_CHANGES, CTX_INLINE_CHAT_POSSIBLE, ACTION_START } from '../common/inlineChat.js'; import { localize, localize2 } from '../../../../nls.js'; import { Action2, IAction2Options, MenuId } from '../../../../platform/actions/common/actions.js'; import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; @@ -49,7 +49,7 @@ export class StartSessionAction extends EditorAction2 { constructor() { super({ - id: 'inlineChat.start', + id: ACTION_START, title: localize2('run', 'Editor Inline Chat'), category: AbstractInlineChatAction.category, f1: true, diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatCurrentLine.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatCurrentLine.ts index d3df7bce374e3..78a792955d137 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatCurrentLine.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatCurrentLine.ts @@ -3,124 +3,49 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { DisposableStore, MutableDisposable } from '../../../../base/common/lifecycle.js'; -import { IActiveCodeEditor, ICodeEditor } from '../../../../editor/browser/editorBrowser.js'; -import { CursorChangeReason } from '../../../../editor/common/cursorEvents.js'; +import { Disposable } from '../../../../base/common/lifecycle.js'; +import { ICodeEditor, MouseTargetType } from '../../../../editor/browser/editorBrowser.js'; import { IEditorContribution } from '../../../../editor/common/editorCommon.js'; import { localize, localize2 } from '../../../../nls.js'; import { ContextKeyExpr, IContextKey, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; -import { IChatAgentService } from '../../chat/common/chatAgents.js'; import { InlineChatController, State } from './inlineChatController.js'; -import { CTX_INLINE_CHAT_HAS_AGENT, CTX_INLINE_CHAT_VISIBLE } from '../common/inlineChat.js'; +import { ACTION_START, CTX_INLINE_CHAT_HAS_AGENT, CTX_INLINE_CHAT_VISIBLE } from '../common/inlineChat.js'; import { EditorAction2, ServicesAccessor } from '../../../../editor/browser/editorExtensions.js'; import { EditOperation } from '../../../../editor/common/core/editOperation.js'; import { Range } from '../../../../editor/common/core/range.js'; -import { Position } from '../../../../editor/common/core/position.js'; +import { IPosition, Position } from '../../../../editor/common/core/position.js'; import { AbstractInlineChatAction } from './inlineChatActions.js'; import { EditorContextKeys } from '../../../../editor/common/editorContextKeys.js'; -import { IValidEditOperation } from '../../../../editor/common/model.js'; - - -export const CTX_INLINE_CHAT_EXPANSION = new RawContextKey('inlineChatExpansion', false, localize('inlineChatExpansion', "Whether the inline chat expansion is enabled when at the end of a just-typed line")); - -export class InlineChatExansionContextKey implements IEditorContribution { - - static Id = 'editor.inlineChatExpansion'; - - private readonly _store = new DisposableStore(); - private readonly _editorListener = this._store.add(new MutableDisposable()); - - private readonly _ctxInlineChatExpansion: IContextKey; - - constructor( - editor: ICodeEditor, - @IContextKeyService contextKeyService: IContextKeyService, - @IChatAgentService chatAgentService: IChatAgentService - ) { - this._ctxInlineChatExpansion = CTX_INLINE_CHAT_EXPANSION.bindTo(contextKeyService); - - const update = () => { - if (editor.hasModel() && chatAgentService.getAgents().length > 0) { - this._install(editor); - } else { - this._uninstall(); - } - }; - this._store.add(chatAgentService.onDidChangeAgents(update)); - this._store.add(editor.onDidChangeModel(update)); - update(); - } - - dispose(): void { - this._ctxInlineChatExpansion.reset(); - this._store.dispose(); - } - - private _install(editor: IActiveCodeEditor): void { - - const store = new DisposableStore(); - this._editorListener.value = store; - - const model = editor.getModel(); - const lastChangeEnds: number[] = []; - - store.add(editor.onDidChangeCursorPosition(e => { - - let enabled = false; - - if (e.reason === CursorChangeReason.NotSet) { - - const position = editor.getPosition(); - const positionOffset = model.getOffsetAt(position); - - const lineLength = model.getLineLength(position.lineNumber); - const firstNonWhitespace = model.getLineFirstNonWhitespaceColumn(position.lineNumber); - - if (firstNonWhitespace !== 0 && position.column > lineLength && lastChangeEnds.includes(positionOffset)) { - enabled = true; - } - } - - lastChangeEnds.length = 0; - this._ctxInlineChatExpansion.set(enabled); - - })); - - store.add(editor.onDidChangeModelContent(e => { - lastChangeEnds.length = 0; - for (const change of e.changes) { - const changeEnd = change.rangeOffset + change.text.length; - lastChangeEnds.push(changeEnd); - } - queueMicrotask(() => { - if (lastChangeEnds.length > 0) { - // this is a signal that onDidChangeCursorPosition didn't run which means some outside change - // which means we should disable the context key - this._ctxInlineChatExpansion.set(false); - } - }); - })); - } - - private _uninstall(): void { - this._editorListener.clear(); - } -} +import { InjectedTextCursorStops, IValidEditOperation, TrackedRangeStickiness } from '../../../../editor/common/model.js'; +import { URI } from '../../../../base/common/uri.js'; +import { isEqual } from '../../../../base/common/resources.js'; +import { StandardTokenType } from '../../../../editor/common/encodedTokenAttributes.js'; +import { autorun, observableFromEvent, observableValue } from '../../../../base/common/observable.js'; +import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js'; +import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js'; +import './media/inlineChat.css'; +import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; +import { ICommandService } from '../../../../platform/commands/common/commands.js'; +import { InlineCompletionsController } from '../../../../editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.js'; + +export const CTX_INLINE_CHAT_SHOWING_HINT = new RawContextKey('inlineChatShowingHint', false, localize('inlineChatShowingHint', "Whether inline chat shows a contextual hint")); + +const _inlineChatActionId = 'inlineChat.startWithCurrentLine'; export class InlineChatExpandLineAction extends EditorAction2 { constructor() { super({ - id: 'inlineChat.startWithCurrentLine', + id: _inlineChatActionId, category: AbstractInlineChatAction.category, title: localize2('startWithCurrentLine', "Start in Editor with Current Line"), f1: true, precondition: ContextKeyExpr.and(CTX_INLINE_CHAT_VISIBLE.negate(), CTX_INLINE_CHAT_HAS_AGENT, EditorContextKeys.writable), - // keybinding: { - // when: CTX_INLINE_CHAT_EXPANSION, - // weight: KeybindingWeight.EditorContrib, - // primary: KeyCode.Tab - // } + keybinding: { + when: CTX_INLINE_CHAT_SHOWING_HINT, + weight: KeybindingWeight.WorkbenchContrib + 1, + primary: KeyMod.CtrlCmd | KeyCode.KeyI + } }); } @@ -164,3 +89,167 @@ export class InlineChatExpandLineAction extends EditorAction2 { } } } + +export class ShowInlineChatHintAction extends EditorAction2 { + + constructor() { + super({ + id: 'inlineChat.showHint', + category: AbstractInlineChatAction.category, + title: localize2('showHint', "Show Inline Chat Hint"), + f1: false, + precondition: ContextKeyExpr.and(CTX_INLINE_CHAT_VISIBLE.negate(), CTX_INLINE_CHAT_HAS_AGENT, EditorContextKeys.writable), + }); + } + + override async runEditorCommand(_accessor: ServicesAccessor, editor: ICodeEditor, ...args: [uri: URI, position: IPosition, ...rest: any[]]) { + if (!editor.hasModel()) { + return; + } + + const ctrl = InlineChatHintsController.get(editor); + if (!ctrl) { + return; + } + + const [uri, position] = args; + if (!URI.isUri(uri) || !Position.isIPosition(position)) { + ctrl.hide(); + return; + } + + const model = editor.getModel(); + if (!isEqual(model.uri, uri)) { + ctrl.hide(); + return; + } + + model.tokenization.forceTokenization(position.lineNumber); + const tokens = model.tokenization.getLineTokens(position.lineNumber); + const tokenIndex = tokens.findTokenIndexAtOffset(position.column - 1); + const tokenType = tokens.getStandardTokenType(tokenIndex); + + if (tokenType === StandardTokenType.Comment) { + ctrl.hide(); + } else { + ctrl.show(); + } + } +} + +export class InlineChatHintsController extends Disposable implements IEditorContribution { + + public static readonly ID = 'editor.contrib.inlineChatHints'; + + static get(editor: ICodeEditor): InlineChatHintsController | null { + return editor.getContribution(InlineChatHintsController.ID); + } + + private readonly _editor: ICodeEditor; + private readonly _ctxShowingHint: IContextKey; + private readonly _visibilityObs = observableValue(this, false); + + constructor( + editor: ICodeEditor, + @IContextKeyService contextKeyService: IContextKeyService, + @ICommandService commandService: ICommandService, + @IKeybindingService keybindingService: IKeybindingService, + ) { + super(); + this._editor = editor; + this._ctxShowingHint = CTX_INLINE_CHAT_SHOWING_HINT.bindTo(contextKeyService); + + + const ghostCtrl = InlineCompletionsController.get(editor); + + this._store.add(this._editor.onMouseDown(e => { + if (e.target.type !== MouseTargetType.CONTENT_TEXT) { + return; + } + if (e.target.detail.injectedText?.options.attachedData !== this) { + return; + } + commandService.executeCommand(_inlineChatActionId); + this.hide(); + })); + + this._store.add(commandService.onWillExecuteCommand(e => { + if (e.commandId === _inlineChatActionId) { + this.hide(); + } + })); + + const posObs = observableFromEvent(editor.onDidChangeCursorPosition, () => editor.getPosition()); + + const decos = this._editor.createDecorationsCollection(); + + const keyObs = observableFromEvent(keybindingService.onDidUpdateKeybindings, _ => keybindingService.lookupKeybinding(ACTION_START)?.getLabel()); + + + this._store.add(autorun(r => { + + const ghostState = ghostCtrl?.model.read(r)?.state.read(r); + const visible = this._visibilityObs.read(r); + const kb = keyObs.read(r); + const position = posObs.read(r); + + // update context key + this._ctxShowingHint.set(visible); + + if (!visible || !kb || !position || ghostState !== undefined) { + decos.clear(); + return; + } + + const column = this._editor.getModel()?.getLineMaxColumn(position.lineNumber); + if (!column) { + return; + } + + const range = Range.fromPositions(position); + + decos.set([{ + range, + options: { + description: 'inline-chat-hint-line', + showIfCollapsed: true, + stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, + after: { + inlineClassName: 'inline-chat-hint', + content: '\u00A0' + localize('ddd', "{0} to chat", kb), + inlineClassNameAffectsLetterSpacing: true, + cursorStops: InjectedTextCursorStops.Both, + attachedData: this + } + } + }]); + })); + } + + show(): void { + this._visibilityObs.set(true, undefined); + } + + hide(): void { + this._visibilityObs.set(false, undefined); + } +} + +export class HideInlineChatHintAction extends EditorAction2 { + + constructor() { + super({ + id: 'inlineChat.hideHint', + title: localize2('hideHint', "Hide Inline Chat Hint"), + precondition: CTX_INLINE_CHAT_SHOWING_HINT, + keybinding: { + weight: KeybindingWeight.EditorContrib - 10, + primary: KeyCode.Escape + } + }); + } + + override async runEditorCommand(_accessor: ServicesAccessor, editor: ICodeEditor): Promise { + InlineChatHintsController.get(editor)?.hide(); + } +} diff --git a/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css index d5c7d7ee031b2..5b10211a2420a 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css +++ b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css @@ -336,3 +336,14 @@ .monaco-workbench .glyph-margin-widgets .cgmr.codicon-inline-chat-transparent:hover { opacity: 1; } + + +/* HINT */ + +.monaco-workbench .monaco-editor .inline-chat-hint { + /* padding: 0 8px; */ + cursor: pointer; + color: var(--vscode-editorGhostText-foreground); + border: 1px solid var(--vscode-editorGhostText-border); + border-radius: 3px; +} diff --git a/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts b/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts index ba46ea0192f3a..e2912601816bb 100644 --- a/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts +++ b/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts @@ -98,6 +98,7 @@ export const CTX_INLINE_CHAT_RESPONSE_TYPE = new RawContextKey Date: Fri, 22 Nov 2024 16:40:25 +0100 Subject: [PATCH 064/119] Git - extract staged resource quick diff provider (#234425) * Git - extract staged resource quick diff provider * Fix the build --- extensions/git/src/blame.ts | 26 +++----------------------- extensions/git/src/main.ts | 9 +++------ extensions/git/src/repository.ts | 31 ++++++++++++++++++++++++++++++- 3 files changed, 36 insertions(+), 30 deletions(-) diff --git a/extensions/git/src/blame.ts b/extensions/git/src/blame.ts index 3e6ad0a1752c4..207ca7b382c0a 100644 --- a/extensions/git/src/blame.ts +++ b/extensions/git/src/blame.ts @@ -3,13 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { DecorationOptions, l10n, Position, Range, TextEditor, TextEditorChange, TextEditorDecorationType, TextEditorChangeKind, ThemeColor, Uri, window, workspace, EventEmitter, ConfigurationChangeEvent, StatusBarItem, StatusBarAlignment, Command, MarkdownString, QuickDiffProvider } from 'vscode'; +import { DecorationOptions, l10n, Position, Range, TextEditor, TextEditorChange, TextEditorDecorationType, TextEditorChangeKind, ThemeColor, Uri, window, workspace, EventEmitter, ConfigurationChangeEvent, StatusBarItem, StatusBarAlignment, Command, MarkdownString } from 'vscode'; import { Model } from './model'; -import { dispose, fromNow, IDisposable, pathEquals } from './util'; +import { dispose, fromNow, IDisposable } from './util'; import { Repository } from './repository'; import { throttle } from './decorators'; import { BlameInformation } from './git'; -import { toGitUri } from './uri'; function lineRangesContainLine(changes: readonly TextEditorChange[], lineNumber: number): boolean { return changes.some(c => c.modified.startLineNumber <= lineNumber && lineNumber < c.modified.endLineNumberExclusive); @@ -63,7 +62,7 @@ interface LineBlameInformation { readonly blameInformation: BlameInformation | string; } -export class GitBlameController implements QuickDiffProvider { +export class GitBlameController { private readonly _onDidChangeBlameInformation = new EventEmitter(); public readonly onDidChangeBlameInformation = this._onDidChangeBlameInformation.event; @@ -87,25 +86,6 @@ export class GitBlameController implements QuickDiffProvider { this._updateTextEditorBlameInformation(window.activeTextEditor); } - get visible(): boolean { - return false; - } - - provideOriginalResource(uri: Uri): Uri | undefined { - // Ignore resources outside a repository - const repository = this._model.getRepository(uri); - if (!repository) { - return undefined; - } - - // Ignore resources that are not in the index group - if (!repository.indexGroup.resourceStates.some(r => pathEquals(r.resourceUri.fsPath, uri.fsPath))) { - return undefined; - } - - return toGitUri(uri, 'HEAD', { replaceFileExtension: true }); - } - getBlameInformationHover(documentUri: Uri, blameInformation: BlameInformation | string): MarkdownString { if (typeof blameInformation === 'string') { return new MarkdownString(blameInformation, true); diff --git a/extensions/git/src/main.ts b/extensions/git/src/main.ts index da5452bf1c367..7180890ad846f 100644 --- a/extensions/git/src/main.ts +++ b/extensions/git/src/main.ts @@ -27,6 +27,7 @@ import { GitPostCommitCommandsProvider } from './postCommitCommands'; import { GitEditSessionIdentityProvider } from './editSessionIdentityProvider'; import { GitCommitInputBoxCodeActionsProvider, GitCommitInputBoxDiagnosticsManager } from './diagnostics'; import { GitBlameController } from './blame'; +import { StagedResourceQuickDiffProvider } from './repository'; const deactivateTasks: { (): Promise }[] = []; @@ -113,17 +114,13 @@ async function createModel(context: ExtensionContext, logger: LogOutputChannel, cc, new GitFileSystemProvider(model), new GitDecorations(model), + new GitBlameController(model), new GitTimelineProvider(model, cc), new GitEditSessionIdentityProvider(model), + new StagedResourceQuickDiffProvider(model), new TerminalShellExecutionManager(model, logger) ); - const blameController = new GitBlameController(model); - disposables.push(blameController); - - const quickDiffProvider = window.registerQuickDiffProvider({ scheme: 'file' }, blameController, 'Git local changes (working tree + index)'); - disposables.push(quickDiffProvider); - const postCommitCommandsProvider = new GitPostCommitCommandsProvider(); model.registerPostCommitCommandsProvider(postCommitCommandsProvider); diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index 795b9f66f0248..58ebba555c26a 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -7,7 +7,7 @@ import TelemetryReporter from '@vscode/extension-telemetry'; import * as fs from 'fs'; import * as path from 'path'; import picomatch from 'picomatch'; -import { CancellationError, CancellationToken, CancellationTokenSource, Command, commands, Disposable, Event, EventEmitter, FileDecoration, l10n, LogLevel, LogOutputChannel, Memento, ProgressLocation, ProgressOptions, RelativePattern, scm, SourceControl, SourceControlInputBox, SourceControlInputBoxValidation, SourceControlInputBoxValidationType, SourceControlResourceDecorations, SourceControlResourceGroup, SourceControlResourceState, TabInputNotebookDiff, TabInputTextDiff, TabInputTextMultiDiff, ThemeColor, Uri, window, workspace, WorkspaceEdit } from 'vscode'; +import { CancellationError, CancellationToken, CancellationTokenSource, Command, commands, Disposable, Event, EventEmitter, FileDecoration, l10n, LogLevel, LogOutputChannel, Memento, ProgressLocation, ProgressOptions, QuickDiffProvider, RelativePattern, scm, SourceControl, SourceControlInputBox, SourceControlInputBoxValidation, SourceControlInputBoxValidationType, SourceControlResourceDecorations, SourceControlResourceGroup, SourceControlResourceState, TabInputNotebookDiff, TabInputTextDiff, TabInputTextMultiDiff, ThemeColor, Uri, window, workspace, WorkspaceEdit } from 'vscode'; import { ActionButton } from './actionButton'; import { ApiRepository } from './api/api1'; import { Branch, BranchQuery, Change, CommitOptions, FetchOptions, ForcePushMode, GitErrorCodes, LogOptions, Ref, RefQuery, RefType, Remote, Status } from './api/git'; @@ -2680,3 +2680,32 @@ export class Repository implements Disposable { this.disposables = dispose(this.disposables); } } + +export class StagedResourceQuickDiffProvider implements QuickDiffProvider { + readonly visible: boolean = false; + + private _disposables: IDisposable[] = []; + + constructor(private readonly _repositoryResolver: IRepositoryResolver) { + this._disposables.push(window.registerQuickDiffProvider({ scheme: 'file' }, this, l10n.t('Git local changes (working tree + index)'))); + } + + provideOriginalResource(uri: Uri): Uri | undefined { + // Ignore resources outside a repository + const repository = this._repositoryResolver.getRepository(uri); + if (!repository) { + return undefined; + } + + // Ignore resources that are not in the index group + if (!repository.indexGroup.resourceStates.some(r => pathEquals(r.resourceUri.fsPath, uri.fsPath))) { + return undefined; + } + + return toGitUri(uri, 'HEAD', { replaceFileExtension: true }); + } + + dispose() { + this._disposables = dispose(this._disposables); + } +} From 36014e42a01c2e1372f338610777f3358ffbd01a Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 22 Nov 2024 07:23:11 -0800 Subject: [PATCH 065/119] Speed up and simplify handling of inline decorations --- src/vs/base/common/color.ts | 2 +- .../browser/gpu/fullFileRenderStrategy.ts | 80 ++++++++----------- 2 files changed, 34 insertions(+), 48 deletions(-) diff --git a/src/vs/base/common/color.ts b/src/vs/base/common/color.ts index 67eeeec757a3e..8b1a68294a507 100644 --- a/src/vs/base/common/color.ts +++ b/src/vs/base/common/color.ts @@ -676,7 +676,7 @@ export namespace Color { const b = parseInt(color.groups?.b ?? '0'); return new Color(new RGBA(r, g, b)); } - // TODO: Support more formats + // TODO: Support more formats as needed return parseNamedKeyword(css); } diff --git a/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts b/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts index bfb27a81d332b..4b3b3c26c0901 100644 --- a/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts +++ b/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts @@ -12,7 +12,7 @@ import type { IViewLineTokens } from '../../common/tokens/lineTokens.js'; import { ViewEventHandler } from '../../common/viewEventHandler.js'; import { ViewEventType, type ViewConfigurationChangedEvent, type ViewDecorationsChangedEvent, type ViewLinesChangedEvent, type ViewLinesDeletedEvent, type ViewLinesInsertedEvent, type ViewScrollChangedEvent, type ViewTokensChangedEvent, type ViewZonesChangedEvent } from '../../common/viewEvents.js'; import type { ViewportData } from '../../common/viewLayout/viewLinesViewportData.js'; -import type { ViewLineRenderingData } from '../../common/viewModel.js'; +import type { InlineDecoration, ViewLineRenderingData } from '../../common/viewModel.js'; import type { ViewContext } from '../../common/viewModel/viewContext.js'; import type { ViewLineOptions } from '../viewParts/viewLines/viewLineOptions.js'; import type { ITextureAtlasPageGlyph } from './atlas/atlas.js'; @@ -233,8 +233,11 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend } update(viewportData: ViewportData, viewLineOptions: ViewLineOptions): number { - // Pre-allocate variables to be shared within the loop - don't trust the JIT compiler to do - // this optimization to avoid additional blocking time in garbage collector + // IMPORTANT: This is a hot function. Variables are pre-allocated and shared within the + // loop. This is done so we don't need to trust the JIT compiler to do this optimization to + // avoid potential additional blocking time in garbage collector which is a common cause of + // dropped frames. + let chars = ''; let y = 0; let x = 0; @@ -251,6 +254,7 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend let charMetadata = 0; let lineData: ViewLineRenderingData; + let decoration: InlineDecoration; let content: string = ''; let fillStartIndex = 0; let fillEndIndex = 0; @@ -315,8 +319,6 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend } } - const decorations = viewportData.getDecorationsInViewport(); - for (y = viewportData.startLineNumber; y <= viewportData.endLineNumber; y++) { // Only attempt to render lines that the GPU renderer can handle @@ -331,14 +333,10 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend if (upToDateLines.has(y)) { continue; } + dirtyLineStart = Math.min(dirtyLineStart, y); dirtyLineEnd = Math.max(dirtyLineEnd, y); - const inlineDecorations = decorations.filter(e => ( - e.range.startLineNumber <= y && e.range.endLineNumber >= y && - e.options.inlineClassName - )); - lineData = viewportData.getViewLineRenderingData(y); content = lineData.content; xOffset = 0; @@ -363,48 +361,36 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend chars = content.charAt(x); charMetadata = 0; - // TODO: We'd want to optimize pulling the decorations in order - const cellDecorations = inlineDecorations.filter(decoration => { - // This is Range.strictContainsPosition except it's working at the cell level. - if (y < decoration.range.startLineNumber || y > decoration.range.endLineNumber) { - return false; - } - if (y === decoration.range.startLineNumber && x < decoration.range.startColumn - 1) { - return false; - } - if (y === decoration.range.endLineNumber && x >= decoration.range.endColumn - 1) { - return false; + // Apply supported inline decoration styles to the cell metadata + for (decoration of lineData.inlineDecorations) { + // This is Range.strictContainsPosition except it works at the cell level, + // it's also inlined to avoid overhead. + if ( + (y < decoration.range.startLineNumber || y > decoration.range.endLineNumber) || + (y === decoration.range.startLineNumber && x < decoration.range.startColumn - 1) || + (y === decoration.range.endLineNumber && x >= decoration.range.endColumn - 1) + ) { + continue; } - return true; - }); - - // Only lines containing fully supported inline decorations should have made it - // this far. - const inlineStyles: Map = new Map(); - for (const decoration of cellDecorations) { - if (!decoration.options.inlineClassName) { - throw new BugIndicatingError('Unexpected inline decoration without class name'); - } - const rules = ViewGpuContext.decorationCssRuleExtractor.getStyleRules(this._viewGpuContext.canvas.domNode, decoration.options.inlineClassName); + + const rules = ViewGpuContext.decorationCssRuleExtractor.getStyleRules(this._viewGpuContext.canvas.domNode, decoration.inlineClassName); for (const rule of rules) { for (const r of rule.style) { - inlineStyles.set(r, rule.styleMap.get(r)?.toString() ?? ''); - } - } - } - - for (const [key, value] of inlineStyles.entries()) { - switch (key) { - case 'color': { - // TODO: This parsing/error handling should move into canRender so fallback to DOM works - const parsedColor = Color.Format.CSS.parse(value); - if (!parsedColor) { - throw new BugIndicatingError('Invalid color format ' + value); + const value = rule.styleMap.get(r)?.toString() ?? ''; + switch (r) { + case 'color': { + // TODO: This parsing and error handling should move into canRender so fallback + // to DOM works + const parsedColor = Color.Format.CSS.parse(value); + if (!parsedColor) { + throw new BugIndicatingError('Invalid color format ' + value); + } + charMetadata = parsedColor.toNumber24Bit(); + break; + } + default: throw new BugIndicatingError('Unexpected inline decoration style'); } - charMetadata = parsedColor.toNumber24Bit(); - break; } - default: throw new BugIndicatingError('Unexpected inline decoration style'); } } From 55e300990234fcbd2e897973d90c77a76a5eb1f1 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 22 Nov 2024 07:33:44 -0800 Subject: [PATCH 066/119] Make canRender call non-static Since this now depends on the DOM, it's too difficult and inconsistent to pass in a DOM node to do the inline decoration test on. It's simplest to pass it in to whatever view parts need it. --- src/vs/editor/browser/gpu/fullFileRenderStrategy.ts | 2 +- src/vs/editor/browser/gpu/raster/glyphRasterizer.ts | 6 ------ src/vs/editor/browser/gpu/viewGpuContext.ts | 8 ++++---- src/vs/editor/browser/view.ts | 4 ++-- src/vs/editor/browser/viewParts/gpuMark/gpuMark.ts | 6 ++---- src/vs/editor/browser/viewParts/viewLines/viewLine.ts | 6 ++---- src/vs/editor/browser/viewParts/viewLines/viewLines.ts | 5 +++-- .../editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts | 4 ++-- 8 files changed, 16 insertions(+), 25 deletions(-) diff --git a/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts b/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts index 4b3b3c26c0901..7ebc66d68e006 100644 --- a/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts +++ b/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts @@ -322,7 +322,7 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend for (y = viewportData.startLineNumber; y <= viewportData.endLineNumber; y++) { // Only attempt to render lines that the GPU renderer can handle - if (!ViewGpuContext.canRender(this._viewGpuContext.canvas.domNode, viewLineOptions, viewportData, y)) { + if (!this._viewGpuContext.canRender(viewLineOptions, viewportData, y)) { fillStartIndex = ((y - 1) * this._viewGpuContext.maxGpuCols) * Constants.IndicesPerCell; fillEndIndex = (y * this._viewGpuContext.maxGpuCols) * Constants.IndicesPerCell; cellBuffer.fill(0, fillStartIndex, fillEndIndex); diff --git a/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts b/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts index a0ae8927b832d..5df78bc465e49 100644 --- a/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts +++ b/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts @@ -127,12 +127,6 @@ export class GlyphRasterizer extends Disposable implements IGlyphRasterizer { this._ctx.textBaseline = 'top'; this._ctx.fillText(chars, originX, originY); - // TODO: Don't draw beyond glyph - how to handle monospace, wide and proportional? - // TODO: Support strikethrough color - if (fontStyle & FontStyle.Strikethrough) { - this._ctx.fillRect(originX, originY + Math.round(devicePixelFontSize / 2), devicePixelFontSize, Math.max(Math.floor(getActiveWindow().devicePixelRatio), 1)); - } - const imageData = this._ctx.getImageData(0, 0, this._canvas.width, this._canvas.height); this._findGlyphBoundingBox(imageData, this._workGlyph.boundingBox); // const offset = { diff --git a/src/vs/editor/browser/gpu/viewGpuContext.ts b/src/vs/editor/browser/gpu/viewGpuContext.ts index 883c75d75bdd6..b7c2bb9cbe82d 100644 --- a/src/vs/editor/browser/gpu/viewGpuContext.ts +++ b/src/vs/editor/browser/gpu/viewGpuContext.ts @@ -141,7 +141,7 @@ export class ViewGpuContext extends Disposable { * renderer. Eventually this should trend all lines, except maybe exceptional cases like * decorations that use class names. */ - public static canRender(container: HTMLElement, options: ViewLineOptions, viewportData: ViewportData, lineNumber: number): boolean { + public canRender(options: ViewLineOptions, viewportData: ViewportData, lineNumber: number): boolean { const data = viewportData.getViewLineRenderingData(lineNumber); // Check if the line has simple attributes that aren't supported @@ -158,7 +158,7 @@ export class ViewGpuContext extends Disposable { if (data.inlineDecorations.length > 0) { let supported = true; for (const decoration of data.inlineDecorations) { - const styleRules = ViewGpuContext._decorationCssRuleExtractor.getStyleRules(container, decoration.inlineClassName); + const styleRules = ViewGpuContext._decorationCssRuleExtractor.getStyleRules(this.canvas.domNode, decoration.inlineClassName); supported &&= styleRules.every(rule => { // Pseudo classes aren't supported currently if (rule.selectorText.includes(':')) { @@ -184,7 +184,7 @@ export class ViewGpuContext extends Disposable { /** * Like {@link canRender} but returns detailed information about why the line cannot be rendered. */ - public static canRenderDetailed(container: HTMLElement, options: ViewLineOptions, viewportData: ViewportData, lineNumber: number): string[] { + public canRenderDetailed(options: ViewLineOptions, viewportData: ViewportData, lineNumber: number): string[] { const data = viewportData.getViewLineRenderingData(lineNumber); const reasons: string[] = []; if (data.containsRTL) { @@ -201,7 +201,7 @@ export class ViewGpuContext extends Disposable { const problemSelectors: string[] = []; const problemRules: string[] = []; for (const decoration of data.inlineDecorations) { - const styleRules = ViewGpuContext._decorationCssRuleExtractor.getStyleRules(container, decoration.inlineClassName); + const styleRules = ViewGpuContext._decorationCssRuleExtractor.getStyleRules(this.canvas.domNode, decoration.inlineClassName); supported &&= styleRules.every(rule => { // Pseudo classes aren't supported currently if (rule.selectorText.includes(':')) { diff --git a/src/vs/editor/browser/view.ts b/src/vs/editor/browser/view.ts index 0b90dab63ed16..10ecd41bb1663 100644 --- a/src/vs/editor/browser/view.ts +++ b/src/vs/editor/browser/view.ts @@ -167,7 +167,7 @@ export class View extends ViewEventHandler { this._viewParts.push(this._scrollbar); // View Lines - this._viewLines = new ViewLines(this._context, this._linesContent); + this._viewLines = new ViewLines(this._context, this._viewGpuContext, this._linesContent); if (this._viewGpuContext) { this._viewLinesGpu = this._instantiationService.createInstance(ViewLinesGpu, this._context, this._viewGpuContext); } @@ -199,7 +199,7 @@ export class View extends ViewEventHandler { marginViewOverlays.addDynamicOverlay(new LinesDecorationsOverlay(this._context)); marginViewOverlays.addDynamicOverlay(new LineNumbersOverlay(this._context)); if (this._viewGpuContext) { - marginViewOverlays.addDynamicOverlay(new GpuMarkOverlay(this._context)); + marginViewOverlays.addDynamicOverlay(new GpuMarkOverlay(this._context, this._viewGpuContext)); } // Glyph margin widgets diff --git a/src/vs/editor/browser/viewParts/gpuMark/gpuMark.ts b/src/vs/editor/browser/viewParts/gpuMark/gpuMark.ts index 861ff529cdc47..733cd8e681a0c 100644 --- a/src/vs/editor/browser/viewParts/gpuMark/gpuMark.ts +++ b/src/vs/editor/browser/viewParts/gpuMark/gpuMark.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { getActiveDocument } from '../../../../base/browser/dom.js'; import * as viewEvents from '../../../common/viewEvents.js'; import { ViewContext } from '../../../common/viewModel/viewContext.js'; import { ViewGpuContext } from '../../gpu/viewGpuContext.js'; @@ -23,7 +22,7 @@ export class GpuMarkOverlay extends DynamicViewOverlay { private _renderResult: string[] | null; - constructor(context: ViewContext) { + constructor(context: ViewContext, private readonly _viewGpuContext: ViewGpuContext) { super(); this._context = context; this._renderResult = null; @@ -78,8 +77,7 @@ export class GpuMarkOverlay extends DynamicViewOverlay { const output: string[] = []; for (let lineNumber = visibleStartLineNumber; lineNumber <= visibleEndLineNumber; lineNumber++) { const lineIndex = lineNumber - visibleStartLineNumber; - // TODO: How to get the container? - const cannotRenderReasons = ViewGpuContext.canRenderDetailed(getActiveDocument().querySelector('.view-lines')!, options, viewportData, lineNumber); + const cannotRenderReasons = this._viewGpuContext.canRenderDetailed(options, viewportData, lineNumber); output[lineIndex] = cannotRenderReasons.length ? `
` : ''; } diff --git a/src/vs/editor/browser/viewParts/viewLines/viewLine.ts b/src/vs/editor/browser/viewParts/viewLines/viewLine.ts index d25238bb4cf54..16229cd928a9a 100644 --- a/src/vs/editor/browser/viewParts/viewLines/viewLine.ts +++ b/src/vs/editor/browser/viewParts/viewLines/viewLine.ts @@ -19,7 +19,6 @@ import { EditorFontLigatures } from '../../../common/config/editorOptions.js'; import { DomReadingContext } from './domReadingContext.js'; import type { ViewLineOptions } from './viewLineOptions.js'; import { ViewGpuContext } from '../../gpu/viewGpuContext.js'; -import { getActiveDocument } from '../../../../base/browser/dom.js'; const canUseFastRenderedViewLine = (function () { if (platform.isNative) { @@ -55,7 +54,7 @@ export class ViewLine implements IVisibleLine { private _isMaybeInvalid: boolean; private _renderedViewLine: IRenderedViewLine | null; - constructor(options: ViewLineOptions) { + constructor(private readonly _viewGpuContext: ViewGpuContext | undefined, options: ViewLineOptions) { this._options = options; this._isMaybeInvalid = true; this._renderedViewLine = null; @@ -99,8 +98,7 @@ export class ViewLine implements IVisibleLine { } public renderLine(lineNumber: number, deltaTop: number, lineHeight: number, viewportData: ViewportData, sb: StringBuilder): boolean { - // TODO: How to get the container? - if (this._options.useGpu && ViewGpuContext.canRender(getActiveDocument().querySelector('.view-lines')!, this._options, viewportData, lineNumber)) { + if (this._options.useGpu && this._viewGpuContext?.canRender(this._options, viewportData, lineNumber)) { this._renderedViewLine?.domNode?.domNode.remove(); this._renderedViewLine = null; return false; diff --git a/src/vs/editor/browser/viewParts/viewLines/viewLines.ts b/src/vs/editor/browser/viewParts/viewLines/viewLines.ts index b93a0028b0bbf..6de14aedb6aeb 100644 --- a/src/vs/editor/browser/viewParts/viewLines/viewLines.ts +++ b/src/vs/editor/browser/viewParts/viewLines/viewLines.ts @@ -25,6 +25,7 @@ import { ViewportData } from '../../../common/viewLayout/viewLinesViewportData.j import { Viewport } from '../../../common/viewModel.js'; import { ViewContext } from '../../../common/viewModel/viewContext.js'; import { ViewLineOptions } from './viewLineOptions.js'; +import type { ViewGpuContext } from '../../gpu/viewGpuContext.js'; class LastRenderedData { @@ -125,7 +126,7 @@ export class ViewLines extends ViewPart implements IViewLines { private _stickyScrollEnabled: boolean; private _maxNumberStickyLines: number; - constructor(context: ViewContext, linesContent: FastDomNode) { + constructor(context: ViewContext, viewGpuContext: ViewGpuContext | undefined, linesContent: FastDomNode) { super(context); const conf = this._context.configuration; @@ -145,7 +146,7 @@ export class ViewLines extends ViewPart implements IViewLines { this._linesContent = linesContent; this._textRangeRestingSpot = document.createElement('div'); this._visibleLines = new VisibleLinesCollection({ - createLine: () => new ViewLine(this._viewLineOptions), + createLine: () => new ViewLine(viewGpuContext, this._viewLineOptions), }); this.domNode = this._visibleLines.domNode; diff --git a/src/vs/editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts b/src/vs/editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts index cdaa217d5e5df..f483e0eda95e6 100644 --- a/src/vs/editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts +++ b/src/vs/editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts @@ -555,7 +555,7 @@ export class ViewLinesGpu extends ViewPart implements IViewLines { if (!this._lastViewportData || !this._lastViewLineOptions) { return undefined; } - if (!ViewGpuContext.canRender(this._viewGpuContext.canvas.domNode, this._lastViewLineOptions, this._lastViewportData, lineNumber)) { + if (!this._viewGpuContext.canRender(this._lastViewLineOptions, this._lastViewportData, lineNumber)) { return undefined; } @@ -573,7 +573,7 @@ export class ViewLinesGpu extends ViewPart implements IViewLines { if (!this._lastViewportData || !this._lastViewLineOptions) { return undefined; } - if (!ViewGpuContext.canRender(this._viewGpuContext.canvas.domNode, this._lastViewLineOptions, this._lastViewportData, lineNumber)) { + if (!this._viewGpuContext.canRender(this._lastViewLineOptions, this._lastViewportData, lineNumber)) { return undefined; } const lineData = this._lastViewportData.getViewLineRenderingData(lineNumber); From 53f4acf633732bd9fc4dca360d120e38a7cecee3 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 22 Nov 2024 07:48:05 -0800 Subject: [PATCH 067/119] Get style extraction working for decorations with multiple class names --- src/vs/editor/browser/gpu/decorationCssRuleExtractor.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/vs/editor/browser/gpu/decorationCssRuleExtractor.ts b/src/vs/editor/browser/gpu/decorationCssRuleExtractor.ts index c05f2d63418d4..9490d2e1e9aeb 100644 --- a/src/vs/editor/browser/gpu/decorationCssRuleExtractor.ts +++ b/src/vs/editor/browser/gpu/decorationCssRuleExtractor.ts @@ -34,7 +34,7 @@ export class DecorationCssRuleExtractor extends Disposable { } // Set up DOM - this._dummyElement.classList.add(decorationClassName); + this._dummyElement.className = decorationClassName; canvas.appendChild(this._container); // Get rules @@ -43,7 +43,6 @@ export class DecorationCssRuleExtractor extends Disposable { // Tear down DOM canvas.removeChild(this._container); - this._dummyElement.classList.remove(decorationClassName); return rules; } From 9be612450eba0fef92eb3b8ac0e4992ed0ad0c0e Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Fri, 22 Nov 2024 18:50:54 +0100 Subject: [PATCH 068/119] update distro (#234438) --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 9c08b11f0c0f0..6b8c2b3f15945 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.96.0", - "distro": "7d14bf7e9a283e1c9ca8b18ccb0c13274a3757cf", + "distro": "83d6d153cc14e1f45b706cdc00383e54f25051bf", "author": { "name": "Microsoft Corporation" }, @@ -240,4 +240,4 @@ "optionalDependencies": { "windows-foreground-love": "0.5.0" } -} +} \ No newline at end of file From 7f65c4eb293c11306f4cb8abf338d13194d19ccb Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Fri, 22 Nov 2024 10:26:47 -0800 Subject: [PATCH 069/119] testing: testing.automaticallyOpenResults -> testing.automaticallyOpenTestResults (#234441) --- src/vs/workbench/contrib/testing/common/configuration.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/testing/common/configuration.ts b/src/vs/workbench/contrib/testing/common/configuration.ts index 9787050f46b19..e0deed662ca62 100644 --- a/src/vs/workbench/contrib/testing/common/configuration.ts +++ b/src/vs/workbench/contrib/testing/common/configuration.ts @@ -13,7 +13,7 @@ import { ConfigurationKeyValuePairs, Extensions, IConfigurationMigrationRegistry export const enum TestingConfigKeys { AutoOpenPeekView = 'testing.automaticallyOpenPeekView', AutoOpenPeekViewDuringContinuousRun = 'testing.automaticallyOpenPeekViewDuringAutoRun', - OpenResults = 'testing.automaticallyOpenResults', + OpenResults = 'testing.automaticallyOpenTestResults', FollowRunningTest = 'testing.followRunningTest', DefaultGutterClickAction = 'testing.defaultGutterClickAction', GutterEnabled = 'testing.gutterEnabled', @@ -200,6 +200,11 @@ Registry.as(Extensions.ConfigurationMigration) migrateFn: (value: AutoOpenTesting): ConfigurationKeyValuePairs => { return [[TestingConfigKeys.OpenResults, { value }]]; } + }, { + key: 'testing.automaticallyOpenResults', // insiders only during 1.96, remove after 1.97 + migrateFn: (value: AutoOpenTesting): ConfigurationKeyValuePairs => { + return [[TestingConfigKeys.OpenResults, { value }]]; + } }]); export interface ITestingCoverageBarThresholds { From 1e9ac16ba979e1436187e593371889bbd3b35588 Mon Sep 17 00:00:00 2001 From: Riley Bruins Date: Fri, 22 Nov 2024 12:11:59 -0800 Subject: [PATCH 070/119] Render JSDoc examples as typescript code (#234143) Currently `@example` produces Markdown code blocks for code examples, but they are not annotated with a programming language. This commit annotates those code blocks as `typescript`, allowing for better highlighting for JS/TS when using Markdown renderers that recognize these languages. --- .../src/languageFeatures/util/textRendering.ts | 2 +- .../src/test/unit/textRendering.test.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/extensions/typescript-language-features/src/languageFeatures/util/textRendering.ts b/extensions/typescript-language-features/src/languageFeatures/util/textRendering.ts index af1a7e601b4e7..f44ac0c4f407c 100644 --- a/extensions/typescript-language-features/src/languageFeatures/util/textRendering.ts +++ b/extensions/typescript-language-features/src/languageFeatures/util/textRendering.ts @@ -28,7 +28,7 @@ function getTagBodyText( if (/^\s*[~`]{3}/m.test(text)) { return text; } - return '```\n' + text + '\n```'; + return '```tsx\n' + text + '\n```'; } let text = convertLinkTags(tag.text, filePathConverter); diff --git a/extensions/typescript-language-features/src/test/unit/textRendering.test.ts b/extensions/typescript-language-features/src/test/unit/textRendering.test.ts index c2a8bafb8c04a..278bf9de412a7 100644 --- a/extensions/typescript-language-features/src/test/unit/textRendering.test.ts +++ b/extensions/typescript-language-features/src/test/unit/textRendering.test.ts @@ -89,7 +89,7 @@ suite('typescript.previewer', () => { text: 'code();' } ], noopToResource), - '*@example* \n```\ncode();\n```' + '*@example* \n```tsx\ncode();\n```' ); }); @@ -113,7 +113,7 @@ suite('typescript.previewer', () => { text: 'Not code\ncode();' } ], noopToResource), - '*@example* \nNot code\n```\ncode();\n```' + '*@example* \nNot code\n```tsx\ncode();\n```' ); }); @@ -154,7 +154,7 @@ suite('typescript.previewer', () => { ] } ], noopToResource), - '*@example* \n```\n1 + 1 {@link foo}\n```'); + '*@example* \n```tsx\n1 + 1 {@link foo}\n```'); }); test('Should render @linkcode symbol name as code', () => { From 2700ee993b9e4b4badac72b4ce03f652ddcf0086 Mon Sep 17 00:00:00 2001 From: Joyce Er Date: Fri, 22 Nov 2024 12:57:59 -0800 Subject: [PATCH 071/119] fix: try decluttering the chat editing widget toolbar (#234448) --- .../chat/browser/chatEditing/chatEditingActions.ts | 14 +++++++------- .../contrib/chat/browser/chatEditorSaving.ts | 4 ++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts index c52b0ae8f7cad..05dfea4541b4c 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts @@ -210,7 +210,7 @@ export class ChatEditingAcceptAllAction extends Action2 { id: MenuId.ChatEditingWidgetToolbar, group: 'navigation', order: 0, - when: ContextKeyExpr.and(applyingChatEditsFailedContextKey.negate(), ContextKeyExpr.or(hasAppliedChatEditsContextKey.negate(), ContextKeyExpr.and(hasUndecidedChatEditingResourceContextKey, ContextKeyExpr.and(ChatContextKeys.location.isEqualTo(ChatAgentLocation.EditingSession))))) + when: ContextKeyExpr.and(applyingChatEditsFailedContextKey.negate(), ContextKeyExpr.and(hasUndecidedChatEditingResourceContextKey, ContextKeyExpr.and(ChatContextKeys.location.isEqualTo(ChatAgentLocation.EditingSession)))) } ] }); @@ -247,7 +247,7 @@ export class ChatEditingDiscardAllAction extends Action2 { id: MenuId.ChatEditingWidgetToolbar, group: 'navigation', order: 1, - when: ContextKeyExpr.and(applyingChatEditsFailedContextKey.negate(), ContextKeyExpr.or(hasAppliedChatEditsContextKey.negate(), ContextKeyExpr.and(ChatContextKeys.location.isEqualTo(ChatAgentLocation.EditingSession), hasUndecidedChatEditingResourceContextKey))) + when: ContextKeyExpr.and(applyingChatEditsFailedContextKey.negate(), ContextKeyExpr.and(ChatContextKeys.location.isEqualTo(ChatAgentLocation.EditingSession), hasUndecidedChatEditingResourceContextKey)) } ], keybinding: { @@ -288,21 +288,21 @@ export class ChatEditingDiscardAllAction extends Action2 { registerAction2(ChatEditingDiscardAllAction); export class ChatEditingRemoveAllFilesAction extends Action2 { - static readonly ID = 'chatEditing.removeAllFiles'; + static readonly ID = 'chatEditing.clearWorkingSet'; constructor() { super({ id: ChatEditingRemoveAllFilesAction.ID, - title: localize('removeAll', 'Remove All'), + title: localize('clearWorkingSet', 'Clear Working Set'), icon: Codicon.clearAll, - tooltip: localize('removeAllFiles', 'Remove All Files'), + tooltip: localize('clearWorkingSet', 'Clear Working Set'), precondition: ContextKeyExpr.and(ChatContextKeys.requestInProgress.negate()), menu: [ { id: MenuId.ChatEditingWidgetToolbar, group: 'navigation', order: 5, - when: ContextKeyExpr.and(ChatContextKeys.location.isEqualTo(ChatAgentLocation.EditingSession)) + when: ContextKeyExpr.and(hasAppliedChatEditsContextKey.negate(), ChatContextKeys.location.isEqualTo(ChatAgentLocation.EditingSession)) } ] }); @@ -344,7 +344,7 @@ export class ChatEditingShowChangesAction extends Action2 { id: MenuId.ChatEditingWidgetToolbar, group: 'navigation', order: 4, - when: ContextKeyExpr.and(applyingChatEditsFailedContextKey.negate(), ContextKeyExpr.or(hasAppliedChatEditsContextKey.negate(), ContextKeyExpr.and(hasAppliedChatEditsContextKey, hasUndecidedChatEditingResourceContextKey, ChatContextKeys.location.isEqualTo(ChatAgentLocation.EditingSession)))) + when: ContextKeyExpr.and(applyingChatEditsFailedContextKey.negate(), ContextKeyExpr.and(hasAppliedChatEditsContextKey, hasUndecidedChatEditingResourceContextKey, ChatContextKeys.location.isEqualTo(ChatAgentLocation.EditingSession))) } ], }); diff --git a/src/vs/workbench/contrib/chat/browser/chatEditorSaving.ts b/src/vs/workbench/contrib/chat/browser/chatEditorSaving.ts index c5c62968a66c0..4d0728bc076a6 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditorSaving.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditorSaving.ts @@ -29,7 +29,7 @@ import { IFilesConfigurationService } from '../../../services/filesConfiguration import { ITextFileService } from '../../../services/textfile/common/textfiles.js'; import { ChatAgentLocation, IChatAgentService } from '../common/chatAgents.js'; import { ChatContextKeys } from '../common/chatContextKeys.js'; -import { applyingChatEditsFailedContextKey, CHAT_EDITING_MULTI_DIFF_SOURCE_RESOLVER_SCHEME, hasAppliedChatEditsContextKey, hasUndecidedChatEditingResourceContextKey, IChatEditingService, IChatEditingSession, IModifiedFileEntry, WorkingSetEntryState } from '../common/chatEditingService.js'; +import { applyingChatEditsFailedContextKey, CHAT_EDITING_MULTI_DIFF_SOURCE_RESOLVER_SCHEME, hasUndecidedChatEditingResourceContextKey, IChatEditingService, IChatEditingSession, IModifiedFileEntry, WorkingSetEntryState } from '../common/chatEditingService.js'; import { IChatModel } from '../common/chatModel.js'; import { IChatService } from '../common/chatService.js'; import { ChatEditingModifiedFileEntry } from './chatEditing/chatEditingModifiedFileEntry.js'; @@ -270,7 +270,7 @@ export class ChatEditingSaveAllAction extends Action2 { // Show the option to save without accepting if the user hasn't configured the setting to always save with generated changes when: ContextKeyExpr.and( applyingChatEditsFailedContextKey.negate(), - ContextKeyExpr.or(hasUndecidedChatEditingResourceContextKey, hasAppliedChatEditsContextKey.negate()), + hasUndecidedChatEditingResourceContextKey, ContextKeyExpr.equals(`config.${ChatEditorSaving._config}`, false), ChatContextKeys.location.isEqualTo(ChatAgentLocation.EditingSession) ) From a0f4e95a8636470eb3609c0b2d95893ab75c89b7 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Fri, 22 Nov 2024 13:02:49 -0800 Subject: [PATCH 072/119] debug: cleanup welcome view actions (#234446) Some consolidation especially now that copilot is also going to be having a welcome view contribution: - Show dynamic configurations in "Run and Debug" as "More X options..." - Remove the separate action for "Show all automatic debug configurations" - Make the "create a launch.json file" start adding a configuration as well (positioning the cursor and triggering completion) - Make "Run and Debug"'s memoization be able to remember dynamic configs ![](https://memes.peet.io/img/24-11-6171dc57-fd60-4165-bcb6-d156bb0517cc.png) fyi @roblourens --- .../debug/browser/debugAdapterManager.ts | 66 +++++++++----- .../browser/debugConfigurationManager.ts | 89 ++++++++++--------- .../contrib/debug/browser/debugService.ts | 74 +++++++++------ .../contrib/debug/browser/debugViewlet.ts | 13 ++- .../contrib/debug/browser/welcomeView.ts | 47 +++++----- .../workbench/contrib/debug/common/debug.ts | 14 ++- .../contrib/debug/common/debugStorage.ts | 14 ++- 7 files changed, 191 insertions(+), 126 deletions(-) diff --git a/src/vs/workbench/contrib/debug/browser/debugAdapterManager.ts b/src/vs/workbench/contrib/debug/browser/debugAdapterManager.ts index 20f77ec9f17af..0a130630a4000 100644 --- a/src/vs/workbench/contrib/debug/browser/debugAdapterManager.ts +++ b/src/vs/workbench/contrib/debug/browser/debugAdapterManager.ts @@ -25,7 +25,7 @@ import { IQuickInputService, IQuickPickItem } from '../../../../platform/quickin import { Registry } from '../../../../platform/registry/common/platform.js'; import { IWorkspaceFolder } from '../../../../platform/workspace/common/workspace.js'; import { Breakpoints } from '../common/breakpoints.js'; -import { CONTEXT_DEBUGGERS_AVAILABLE, CONTEXT_DEBUG_EXTENSION_AVAILABLE, IAdapterDescriptor, IAdapterManager, IConfig, IDebugAdapter, IDebugAdapterDescriptorFactory, IDebugAdapterFactory, IDebugConfiguration, IDebugSession, INTERNAL_CONSOLE_OPTIONS_SCHEMA } from '../common/debug.js'; +import { CONTEXT_DEBUGGERS_AVAILABLE, CONTEXT_DEBUG_EXTENSION_AVAILABLE, IAdapterDescriptor, IAdapterManager, IConfig, IConfigurationManager, IDebugAdapter, IDebugAdapterDescriptorFactory, IDebugAdapterFactory, IDebugConfiguration, IDebugSession, IGuessedDebugger, INTERNAL_CONSOLE_OPTIONS_SCHEMA } from '../common/debug.js'; import { Debugger } from '../common/debugger.js'; import { breakpointsExtPoint, debuggersExtPoint, launchSchema, presentationSchema } from '../common/debugSchemas.js'; import { TaskDefinitionRegistry } from '../../tasks/common/taskDefinitionRegistry.js'; @@ -39,6 +39,7 @@ const jsonRegistry = Registry.as(JSONExtensions.JSONC export interface IAdapterManagerDelegate { onDidNewSession: Event; + configurationManager(): IConfigurationManager; } export class AdapterManager extends Disposable implements IAdapterManager { @@ -60,7 +61,7 @@ export class AdapterManager extends Disposable implements IAdapterManager { private usedDebugTypes = new Set(); constructor( - delegate: IAdapterManagerDelegate, + private readonly delegate: IAdapterManagerDelegate, @IEditorService private readonly editorService: IEditorService, @IConfigurationService private readonly configurationService: IConfigurationService, @IQuickInputService private readonly quickInputService: IQuickInputService, @@ -340,7 +341,7 @@ export class AdapterManager extends Disposable implements IAdapterManager { .find(a => a.interestedInLanguage(languageId)); } - async guessDebugger(gettingConfigurations: boolean): Promise { + async guessDebugger(gettingConfigurations: boolean): Promise { const activeTextEditorControl = this.editorService.activeTextEditorControl; let candidates: Debugger[] = []; let languageLabel: string | null = null; @@ -355,7 +356,7 @@ export class AdapterManager extends Disposable implements IAdapterManager { .filter(a => a.enabled) .filter(a => language && a.interestedInLanguage(language)); if (adapters.length === 1) { - return adapters[0]; + return { debugger: adapters[0] }; } if (adapters.length > 1) { candidates = adapters; @@ -407,11 +408,12 @@ export class AdapterManager extends Disposable implements IAdapterManager { } }); - const picks: ({ label: string; debugger?: Debugger; type?: string } | MenuItemAction)[] = []; + const picks: ({ label: string; pick?: () => IGuessedDebugger | Promise; type?: string } | MenuItemAction)[] = []; + const dynamic = await this.delegate.configurationManager().getDynamicProviders(); if (suggestedCandidates.length > 0) { picks.push( { type: 'separator', label: nls.localize('suggestedDebuggers', "Suggested") }, - ...suggestedCandidates.map(c => ({ label: c.label, debugger: c }))); + ...suggestedCandidates.map(c => ({ label: c.label, pick: () => ({ debugger: c }) }))); } if (otherCandidates.length > 0) { @@ -419,12 +421,30 @@ export class AdapterManager extends Disposable implements IAdapterManager { picks.push({ type: 'separator', label: '' }); } - picks.push(...otherCandidates.map(c => ({ label: c.label, debugger: c }))); + picks.push(...otherCandidates.map(c => ({ label: c.label, pick: () => ({ debugger: c }) }))); + } + + if (dynamic.length) { + if (picks.length) { + picks.push({ type: 'separator', label: '' }); + } + + for (const d of dynamic) { + picks.push({ + label: nls.localize('moreOptionsForDebugType', "More {0} options...", d.label), + pick: async (): Promise => { + const cfg = await d.pick(); + if (!cfg) { return undefined; } + return cfg && { debugger: this.getDebugger(d.type)!, withConfig: cfg }; + }, + }); + } } picks.push( { type: 'separator', label: '' }, - { label: languageLabel ? nls.localize('installLanguage', "Install an extension for {0}...", languageLabel) : nls.localize('installExt', "Install extension...") }); + { label: languageLabel ? nls.localize('installLanguage', "Install an extension for {0}...", languageLabel) : nls.localize('installExt', "Install extension...") } + ); const contributed = this.menuService.getMenuActions(MenuId.DebugCreateConfiguration, this.contextKeyService); for (const [, action] of contributed) { @@ -432,20 +452,24 @@ export class AdapterManager extends Disposable implements IAdapterManager { picks.push(item); } } + const placeHolder = nls.localize('selectDebug', "Select debugger"); - return this.quickInputService.pick<{ label: string; debugger?: Debugger } | IQuickPickItem>(picks, { activeItem: picks[0], placeHolder }) - .then(async picked => { - if (picked && 'debugger' in picked && picked.debugger) { - return picked.debugger; - } else if (picked instanceof MenuItemAction) { - picked.run(); - return; - } - if (picked) { - this.commandService.executeCommand('debug.installAdditionalDebuggers', languageLabel); - } - return undefined; - }); + return this.quickInputService.pick<{ label: string; debugger?: Debugger } | IQuickPickItem>(picks, { activeItem: picks[0], placeHolder }).then(async picked => { + if (picked && 'pick' in picked && typeof picked.pick === 'function') { + return await picked.pick(); + } + + if (picked instanceof MenuItemAction) { + picked.run(); + return; + } + + if (picked) { + this.commandService.executeCommand('debug.installAdditionalDebuggers', languageLabel); + } + + return undefined; + }); } private initExtensionActivationsIfNeeded(): void { diff --git a/src/vs/workbench/contrib/debug/browser/debugConfigurationManager.ts b/src/vs/workbench/contrib/debug/browser/debugConfigurationManager.ts index 26d6f90be38bc..0cc18f401f1a3 100644 --- a/src/vs/workbench/contrib/debug/browser/debugConfigurationManager.ts +++ b/src/vs/workbench/contrib/debug/browser/debugConfigurationManager.ts @@ -28,7 +28,7 @@ import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uri import { IWorkspaceContextService, IWorkspaceFolder, IWorkspaceFoldersChangeEvent, WorkbenchState } from '../../../../platform/workspace/common/workspace.js'; import { IEditorPane } from '../../../common/editor.js'; import { debugConfigure } from './debugIcons.js'; -import { CONTEXT_DEBUG_CONFIGURATION_TYPE, DebugConfigurationProviderTriggerKind, IAdapterManager, ICompound, IConfig, IConfigPresentation, IConfigurationManager, IDebugConfigurationProvider, IGlobalConfig, ILaunch } from '../common/debug.js'; +import { CONTEXT_DEBUG_CONFIGURATION_TYPE, DebugConfigurationProviderTriggerKind, IAdapterManager, ICompound, IConfig, IConfigPresentation, IConfigurationManager, IDebugConfigurationProvider, IGlobalConfig, IGuessedDebugger, ILaunch } from '../common/debug.js'; import { launchSchema } from '../common/debugSchemas.js'; import { getVisibleAndSorted } from '../common/debugUtils.js'; import { launchSchemaId } from '../../../services/configuration/common/configuration.js'; @@ -46,6 +46,7 @@ const DEBUG_SELECTED_ROOT = 'debug.selectedroot'; // Debug type is only stored if a dynamic configuration is used for better restore const DEBUG_SELECTED_TYPE = 'debug.selectedtype'; const DEBUG_RECENT_DYNAMIC_CONFIGURATIONS = 'debug.recentdynamicconfigurations'; +const ON_DEBUG_DYNAMIC_CONFIGURATIONS_NAME = 'onDebugDynamicConfigurations'; interface IDynamicPickItem { label: string; launch: ILaunch; config: IConfig } @@ -174,9 +175,8 @@ export class ConfigurationManager implements IConfigurationManager { return results.reduce((first, second) => first.concat(second), []); } - async getDynamicProviders(): Promise<{ label: string; type: string; getProvider: () => Promise; pick: () => Promise<{ launch: ILaunch; config: IConfig } | undefined> }[]> { + async getDynamicProviders(): Promise<{ label: string; type: string; getProvider: () => Promise; pick: () => Promise<{ launch: ILaunch; config: IConfig; label: string } | undefined> }[]> { await this.extensionService.whenInstalledExtensionsRegistered(); - const onDebugDynamicConfigurationsName = 'onDebugDynamicConfigurations'; const debugDynamicExtensionsTypes = this.extensionService.extensions.reduce((acc, e) => { if (!e.activationEvents) { return acc; @@ -185,10 +185,10 @@ export class ConfigurationManager implements IConfigurationManager { const explicitTypes: string[] = []; let hasGenericEvent = false; for (const event of e.activationEvents) { - if (event === onDebugDynamicConfigurationsName) { + if (event === ON_DEBUG_DYNAMIC_CONFIGURATIONS_NAME) { hasGenericEvent = true; - } else if (event.startsWith(`${onDebugDynamicConfigurationsName}:`)) { - explicitTypes.push(event.slice(onDebugDynamicConfigurationsName.length + 1)); + } else if (event.startsWith(`${ON_DEBUG_DYNAMIC_CONFIGURATIONS_NAME}:`)) { + explicitTypes.push(event.slice(ON_DEBUG_DYNAMIC_CONFIGURATIONS_NAME.length + 1)); } } @@ -214,33 +214,17 @@ export class ConfigurationManager implements IConfigurationManager { return { label: this.adapterManager.getDebuggerLabel(type)!, getProvider: async () => { - await this.adapterManager.activateDebuggers(onDebugDynamicConfigurationsName, type); + await this.adapterManager.activateDebuggers(ON_DEBUG_DYNAMIC_CONFIGURATIONS_NAME, type); return this.configProviders.find(p => p.type === type && p.triggerKind === DebugConfigurationProviderTriggerKind.Dynamic && p.provideDebugConfigurations); }, type, pick: async () => { // Do a late 'onDebugDynamicConfigurationsName' activation so extensions are not activated too early #108578 - await this.adapterManager.activateDebuggers(onDebugDynamicConfigurationsName, type); - - const token = new CancellationTokenSource(); - const picks: Promise[] = []; - const provider = this.configProviders.find(p => p.type === type && p.triggerKind === DebugConfigurationProviderTriggerKind.Dynamic && p.provideDebugConfigurations); - this.getLaunches().forEach(launch => { - if (provider) { - picks.push(provider.provideDebugConfigurations!(launch.workspace?.uri, token.token).then(configurations => configurations.map(config => ({ - label: config.name, - description: launch.name, - config, - buttons: [{ - iconClass: ThemeIcon.asClassName(debugConfigure), - tooltip: nls.localize('editLaunchConfig', "Edit Debug Configuration in launch.json") - }], - launch - })))); - } - }); + await this.adapterManager.activateDebuggers(ON_DEBUG_DYNAMIC_CONFIGURATIONS_NAME, type); const disposables = new DisposableStore(); + const token = new CancellationTokenSource(); + disposables.add(token); const input = disposables.add(this.quickInputService.createQuickPick()); input.busy = true; input.placeholder = nls.localize('selectConfiguration', "Select Launch Configuration"); @@ -257,41 +241,56 @@ export class ConfigurationManager implements IConfigurationManager { this.removeRecentDynamicConfigurations(config.name, config.type); })); disposables.add(input.onDidHide(() => resolve(undefined))); - }); + }).finally(() => token.cancel()); - let nestedPicks: IDynamicPickItem[][]; + let items: IDynamicPickItem[]; try { // This await invokes the extension providers, which might fail due to several reasons, // therefore we gate this logic under a try/catch to prevent leaving the Debug Tab // selector in a borked state. - nestedPicks = await Promise.all(picks); + items = await this.getDynamicConfigurationsByType(type, token.token); } catch (err) { this.logService.error(err); disposables.dispose(); return; } - const items = nestedPicks.flat(); - input.items = items; input.busy = false; input.show(); const chosen = await chosenPromise; - disposables.dispose(); - if (!chosen) { - // User canceled quick input we should notify the provider to cancel computing configurations - token.cancel(); - return; - } - return chosen; } }; }); } + async getDynamicConfigurationsByType(type: string, token: CancellationToken = CancellationToken.None): Promise { + // Do a late 'onDebugDynamicConfigurationsName' activation so extensions are not activated too early #108578 + await this.adapterManager.activateDebuggers(ON_DEBUG_DYNAMIC_CONFIGURATIONS_NAME, type); + + const picks: Promise[] = []; + const provider = this.configProviders.find(p => p.type === type && p.triggerKind === DebugConfigurationProviderTriggerKind.Dynamic && p.provideDebugConfigurations); + this.getLaunches().forEach(launch => { + if (provider) { + picks.push(provider.provideDebugConfigurations!(launch.workspace?.uri, token).then(configurations => configurations.map(config => ({ + label: config.name, + description: launch.name, + config, + buttons: [{ + iconClass: ThemeIcon.asClassName(debugConfigure), + tooltip: nls.localize('editLaunchConfig', "Edit Debug Configuration in launch.json") + }], + launch + })))); + } + }); + + return (await Promise.all(picks)).flat(); + } + getAllConfigurations(): { launch: ILaunch; name: string; presentation?: IConfigPresentation }[] { const all: { launch: ILaunch; name: string; presentation?: IConfigPresentation }[] = []; for (const l of this.launches) { @@ -560,13 +559,19 @@ abstract class AbstractLaunch implements ILaunch { async getInitialConfigurationContent(folderUri?: uri, type?: string, useInitialConfigs?: boolean, token?: CancellationToken): Promise { let content = ''; - const adapter = type ? this.adapterManager.getEnabledDebugger(type) : await this.adapterManager.guessDebugger(true); - if (adapter) { + const adapter: Partial | undefined = type + ? { debugger: this.adapterManager.getEnabledDebugger(type) } + : await this.adapterManager.guessDebugger(true); + + if (adapter?.withConfig && adapter.debugger) { + content = await adapter.debugger.getInitialConfigurationContent([adapter.withConfig.config]); + } else if (adapter?.debugger) { const initialConfigs = useInitialConfigs ? - await this.configurationManager.provideDebugConfigurations(folderUri, adapter.type, token || CancellationToken.None) : + await this.configurationManager.provideDebugConfigurations(folderUri, adapter.debugger.type, token || CancellationToken.None) : []; - content = await adapter.getInitialConfigurationContent(initialConfigs); + content = await adapter.debugger.getInitialConfigurationContent(initialConfigs); } + return content; } diff --git a/src/vs/workbench/contrib/debug/browser/debugService.ts b/src/vs/workbench/contrib/debug/browser/debugService.ts index 44f17e95d19aa..7019624359ca2 100644 --- a/src/vs/workbench/contrib/debug/browser/debugService.ts +++ b/src/vs/workbench/contrib/debug/browser/debugService.ts @@ -34,24 +34,6 @@ import { IWorkspaceTrustRequestService } from '../../../../platform/workspace/co import { EditorsOrder } from '../../../common/editor.js'; import { EditorInput } from '../../../common/editor/editorInput.js'; import { IViewDescriptorService, ViewContainerLocation } from '../../../common/views.js'; -import { AdapterManager } from './debugAdapterManager.js'; -import { DEBUG_CONFIGURE_COMMAND_ID, DEBUG_CONFIGURE_LABEL } from './debugCommands.js'; -import { ConfigurationManager } from './debugConfigurationManager.js'; -import { DebugMemoryFileSystemProvider } from './debugMemory.js'; -import { DebugSession } from './debugSession.js'; -import { DebugTaskRunner, TaskRunResult } from './debugTaskRunner.js'; -import { CALLSTACK_VIEW_ID, CONTEXT_BREAKPOINTS_EXIST, CONTEXT_DEBUG_STATE, CONTEXT_DEBUG_TYPE, CONTEXT_DEBUG_UX, CONTEXT_DISASSEMBLY_VIEW_FOCUS, CONTEXT_HAS_DEBUGGED, CONTEXT_IN_DEBUG_MODE, DEBUG_MEMORY_SCHEME, DEBUG_SCHEME, IAdapterManager, IBreakpoint, IBreakpointData, IBreakpointUpdateData, ICompound, IConfig, IConfigurationManager, IDebugConfiguration, IDebugModel, IDebugService, IDebugSession, IDebugSessionOptions, IEnablement, IExceptionBreakpoint, IGlobalConfig, ILaunch, IStackFrame, IThread, IViewModel, REPL_VIEW_ID, State, VIEWLET_ID, debuggerDisabledMessage, getStateLabel } from '../common/debug.js'; -import { DebugCompoundRoot } from '../common/debugCompoundRoot.js'; -import { Breakpoint, DataBreakpoint, DebugModel, FunctionBreakpoint, IDataBreakpointOptions, IFunctionBreakpointOptions, IInstructionBreakpointOptions, InstructionBreakpoint } from '../common/debugModel.js'; -import { Source } from '../common/debugSource.js'; -import { DebugStorage } from '../common/debugStorage.js'; -import { DebugTelemetry } from '../common/debugTelemetry.js'; -import { getExtensionHostDebugSession, saveAllBeforeDebugStart } from '../common/debugUtils.js'; -import { ViewModel } from '../common/debugViewModel.js'; -import { Debugger } from '../common/debugger.js'; -import { DisassemblyViewInput } from '../common/disassemblyViewInput.js'; -import { VIEWLET_ID as EXPLORER_VIEWLET_ID } from '../../files/common/files.js'; -import { ITestService } from '../../testing/common/testService.js'; import { IActivityService, NumberBadge } from '../../../services/activity/common/activity.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; import { IExtensionService } from '../../../services/extensions/common/extensions.js'; @@ -59,6 +41,23 @@ import { IWorkbenchLayoutService, Parts } from '../../../services/layout/browser import { ILifecycleService } from '../../../services/lifecycle/common/lifecycle.js'; import { IPaneCompositePartService } from '../../../services/panecomposite/browser/panecomposite.js'; import { IViewsService } from '../../../services/views/common/viewsService.js'; +import { VIEWLET_ID as EXPLORER_VIEWLET_ID } from '../../files/common/files.js'; +import { ITestService } from '../../testing/common/testService.js'; +import { CALLSTACK_VIEW_ID, CONTEXT_BREAKPOINTS_EXIST, CONTEXT_DEBUG_STATE, CONTEXT_DEBUG_TYPE, CONTEXT_DEBUG_UX, CONTEXT_DISASSEMBLY_VIEW_FOCUS, CONTEXT_HAS_DEBUGGED, CONTEXT_IN_DEBUG_MODE, DEBUG_MEMORY_SCHEME, DEBUG_SCHEME, IAdapterManager, IBreakpoint, IBreakpointData, IBreakpointUpdateData, ICompound, IConfig, IConfigurationManager, IDebugConfiguration, IDebugModel, IDebugService, IDebugSession, IDebugSessionOptions, IEnablement, IExceptionBreakpoint, IGlobalConfig, IGuessedDebugger, ILaunch, IStackFrame, IThread, IViewModel, REPL_VIEW_ID, State, VIEWLET_ID, debuggerDisabledMessage, getStateLabel } from '../common/debug.js'; +import { DebugCompoundRoot } from '../common/debugCompoundRoot.js'; +import { Breakpoint, DataBreakpoint, DebugModel, FunctionBreakpoint, IDataBreakpointOptions, IFunctionBreakpointOptions, IInstructionBreakpointOptions, InstructionBreakpoint } from '../common/debugModel.js'; +import { Source } from '../common/debugSource.js'; +import { DebugStorage, IChosenEnvironment } from '../common/debugStorage.js'; +import { DebugTelemetry } from '../common/debugTelemetry.js'; +import { getExtensionHostDebugSession, saveAllBeforeDebugStart } from '../common/debugUtils.js'; +import { ViewModel } from '../common/debugViewModel.js'; +import { DisassemblyViewInput } from '../common/disassemblyViewInput.js'; +import { AdapterManager } from './debugAdapterManager.js'; +import { DEBUG_CONFIGURE_COMMAND_ID, DEBUG_CONFIGURE_LABEL } from './debugCommands.js'; +import { ConfigurationManager } from './debugConfigurationManager.js'; +import { DebugMemoryFileSystemProvider } from './debugMemory.js'; +import { DebugSession } from './debugSession.js'; +import { DebugTaskRunner, TaskRunResult } from './debugTaskRunner.js'; export class DebugService implements IDebugService { declare readonly _serviceBrand: undefined; @@ -89,7 +88,7 @@ export class DebugService implements IDebugService { private previousState: State | undefined; private sessionCancellationTokens = new Map(); private activity: IDisposable | undefined; - private chosenEnvironments: { [key: string]: string }; + private chosenEnvironments: Record; private haveDoneLazySetup = false; constructor( @@ -122,7 +121,10 @@ export class DebugService implements IDebugService { this._onWillNewSession = new Emitter(); this._onDidEndSession = new Emitter(); - this.adapterManager = this.instantiationService.createInstance(AdapterManager, { onDidNewSession: this.onDidNewSession }); + this.adapterManager = this.instantiationService.createInstance(AdapterManager, { + onDidNewSession: this.onDidNewSession, + configurationManager: () => this.configurationManager, + }); this.disposables.add(this.adapterManager); this.configurationManager = this.instantiationService.createInstance(ConfigurationManager, this.adapterManager); this.disposables.add(this.configurationManager); @@ -457,26 +459,42 @@ export class DebugService implements IDebugService { type = config.type; } else { // a no-folder workspace has no launch.config - config = Object.create(null); + config = Object.create(null) as IConfig; } if (options && options.noDebug) { - config!.noDebug = true; + config.noDebug = true; } else if (options && typeof options.noDebug === 'undefined' && options.parentSession && options.parentSession.configuration.noDebug) { - config!.noDebug = true; + config.noDebug = true; } const unresolvedConfig = deepClone(config); - let guess: Debugger | undefined; + let guess: IGuessedDebugger | undefined; let activeEditor: EditorInput | undefined; if (!type) { activeEditor = this.editorService.activeEditor; if (activeEditor && activeEditor.resource) { - type = this.chosenEnvironments[activeEditor.resource.toString()]; + const chosen = this.chosenEnvironments[activeEditor.resource.toString()]; + if (chosen) { + type = chosen.type; + if (chosen.dynamicLabel) { + const dyn = await this.configurationManager.getDynamicConfigurationsByType(chosen.type); + const found = dyn.find(d => d.label === chosen.dynamicLabel); + if (found) { + launch = found.launch; + Object.assign(config, found.config); + } + } + } } + if (!type) { guess = await this.adapterManager.guessDebugger(false); if (guess) { - type = guess.type; + type = guess.debugger.type; + if (guess.withConfig) { + launch = guess.withConfig.launch; + Object.assign(config, guess.withConfig.config); + } } } } @@ -485,7 +503,7 @@ export class DebugService implements IDebugService { const sessionId = generateUuid(); this.sessionCancellationTokens.set(sessionId, initCancellationToken); - const configByProviders = await this.configurationManager.resolveConfigurationByProviders(launch && launch.workspace ? launch.workspace.uri : undefined, type, config!, initCancellationToken.token); + const configByProviders = await this.configurationManager.resolveConfigurationByProviders(launch && launch.workspace ? launch.workspace.uri : undefined, type, config, initCancellationToken.token); // a falsy config indicates an aborted launch if (configByProviders && configByProviders.type) { try { @@ -550,7 +568,7 @@ export class DebugService implements IDebugService { const result = await this.doCreateSession(sessionId, launch?.workspace, { resolved: resolvedConfig, unresolved: unresolvedConfig }, options); if (result && guess && activeEditor && activeEditor.resource) { // Remeber user choice of environment per active editor to make starting debugging smoother #124770 - this.chosenEnvironments[activeEditor.resource.toString()] = guess.type; + this.chosenEnvironments[activeEditor.resource.toString()] = { type: guess.debugger.type, dynamicLabel: guess.withConfig?.label }; this.debugStorage.storeChosenEnvironments(this.chosenEnvironments); } return result; diff --git a/src/vs/workbench/contrib/debug/browser/debugViewlet.ts b/src/vs/workbench/contrib/debug/browser/debugViewlet.ts index dc205a0093ba2..6db69ee2dfba9 100644 --- a/src/vs/workbench/contrib/debug/browser/debugViewlet.ts +++ b/src/vs/workbench/contrib/debug/browser/debugViewlet.ts @@ -30,10 +30,11 @@ import { DEBUG_CONFIGURE_COMMAND_ID, DEBUG_CONFIGURE_LABEL, DEBUG_START_COMMAND_ import { debugConfigure } from './debugIcons.js'; import { createDisconnectMenuItemAction } from './debugToolBar.js'; import { WelcomeView } from './welcomeView.js'; -import { BREAKPOINTS_VIEW_ID, CONTEXT_DEBUGGERS_AVAILABLE, CONTEXT_DEBUG_STATE, CONTEXT_DEBUG_UX, CONTEXT_DEBUG_UX_KEY, getStateLabel, IDebugService, ILaunch, REPL_VIEW_ID, State, VIEWLET_ID } from '../common/debug.js'; +import { BREAKPOINTS_VIEW_ID, CONTEXT_DEBUGGERS_AVAILABLE, CONTEXT_DEBUG_STATE, CONTEXT_DEBUG_UX, CONTEXT_DEBUG_UX_KEY, getStateLabel, IDebugService, ILaunch, REPL_VIEW_ID, State, VIEWLET_ID, EDITOR_CONTRIBUTION_ID, IDebugEditorContribution } from '../common/debug.js'; import { IExtensionService } from '../../../services/extensions/common/extensions.js'; import { IWorkbenchLayoutService } from '../../../services/layout/browser/layoutService.js'; import { IBaseActionViewItemOptions } from '../../../../base/browser/ui/actionbar/actionViewItems.js'; +import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js'; export class DebugViewPaneContainer extends ViewPaneContainer { @@ -223,7 +224,7 @@ registerAction2(class extends Action2 { }); } - async run(accessor: ServicesAccessor): Promise { + async run(accessor: ServicesAccessor, opts?: { addNew?: boolean }): Promise { const debugService = accessor.get(IDebugService); const quickInputService = accessor.get(IQuickInputService); const configurationManager = debugService.getConfigurationManager(); @@ -247,7 +248,13 @@ registerAction2(class extends Action2 { } if (launch) { - await launch.openConfigFile({ preserveFocus: false }); + const { editor } = await launch.openConfigFile({ preserveFocus: false }); + if (editor && opts?.addNew) { + const codeEditor = editor.getControl(); + if (codeEditor) { + await codeEditor.getContribution(EDITOR_CONTRIBUTION_ID)?.addLaunchConfiguration(); + } + } } } }); diff --git a/src/vs/workbench/contrib/debug/browser/welcomeView.ts b/src/vs/workbench/contrib/debug/browser/welcomeView.ts index 7dd6a939bb3fc..f1ca97ff5e6d5 100644 --- a/src/vs/workbench/contrib/debug/browser/welcomeView.ts +++ b/src/vs/workbench/contrib/debug/browser/welcomeView.ts @@ -3,30 +3,30 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IViewletViewOptions } from '../../../browser/parts/views/viewsViewlet.js'; -import { IThemeService } from '../../../../platform/theme/common/themeService.js'; -import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; -import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js'; -import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; -import { IContextKeyService, RawContextKey, IContextKey, ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; +import { DisposableStore } from '../../../../base/common/lifecycle.js'; +import { isMacintosh, isWeb } from '../../../../base/common/platform.js'; +import { isCodeEditor, isDiffEditor } from '../../../../editor/browser/editorBrowser.js'; import { localize, localize2 } from '../../../../nls.js'; -import { IDebugService, CONTEXT_DEBUGGERS_AVAILABLE, CONTEXT_DEBUG_EXTENSION_AVAILABLE } from '../common/debug.js'; -import { IEditorService } from '../../../services/editor/common/editorService.js'; -import { ViewPane } from '../../../browser/parts/views/viewPane.js'; +import { ILocalizedString } from '../../../../platform/action/common/action.js'; +import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; +import { ContextKeyExpr, IContextKey, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; +import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js'; +import { IHoverService } from '../../../../platform/hover/browser/hover.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; -import { IViewDescriptorService, IViewsRegistry, Extensions, ViewContentGroups } from '../../../common/views.js'; -import { Registry } from '../../../../platform/registry/common/platform.js'; +import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; import { IOpenerService } from '../../../../platform/opener/common/opener.js'; -import { WorkbenchStateContext } from '../../../common/contextkeys.js'; -import { OpenFolderAction, OpenFileAction, OpenFileFolderAction } from '../../../browser/actions/workspaceActions.js'; -import { isMacintosh, isWeb } from '../../../../base/common/platform.js'; -import { isCodeEditor, isDiffEditor } from '../../../../editor/browser/editorBrowser.js'; +import { Registry } from '../../../../platform/registry/common/platform.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; -import { DisposableStore } from '../../../../base/common/lifecycle.js'; -import { SELECT_AND_START_ID, DEBUG_CONFIGURE_COMMAND_ID, DEBUG_START_COMMAND_ID } from './debugCommands.js'; -import { ILocalizedString } from '../../../../platform/action/common/action.js'; -import { IHoverService } from '../../../../platform/hover/browser/hover.js'; +import { IThemeService } from '../../../../platform/theme/common/themeService.js'; +import { OpenFileAction, OpenFileFolderAction, OpenFolderAction } from '../../../browser/actions/workspaceActions.js'; +import { ViewPane } from '../../../browser/parts/views/viewPane.js'; +import { IViewletViewOptions } from '../../../browser/parts/views/viewsViewlet.js'; +import { WorkbenchStateContext } from '../../../common/contextkeys.js'; +import { Extensions, IViewDescriptorService, IViewsRegistry, ViewContentGroups } from '../../../common/views.js'; +import { IEditorService } from '../../../services/editor/common/editorService.js'; +import { CONTEXT_DEBUGGERS_AVAILABLE, CONTEXT_DEBUG_EXTENSION_AVAILABLE, IDebugService } from '../common/debug.js'; +import { DEBUG_CONFIGURE_COMMAND_ID, DEBUG_START_COMMAND_ID } from './debugCommands.js'; const debugStartLanguageKey = 'debugStartLanguage'; const CONTEXT_DEBUG_START_LANGUAGE = new RawContextKey(debugStartLanguageKey, undefined); @@ -141,13 +141,6 @@ viewsRegistry.registerViewWelcomeContent(WelcomeView.ID, { order: 1 }); -viewsRegistry.registerViewWelcomeContent(WelcomeView.ID, { - content: `[${localize('detectThenRunAndDebug', "Show all automatic debug configurations")}](command:${SELECT_AND_START_ID}).`, - when: CONTEXT_DEBUGGERS_AVAILABLE, - group: ViewContentGroups.Debug, - order: 10 -}); - viewsRegistry.registerViewWelcomeContent(WelcomeView.ID, { content: localize( { @@ -157,7 +150,7 @@ viewsRegistry.registerViewWelcomeContent(WelcomeView.ID, { '{Locked="](command:{0})"}' ] }, - "To customize Run and Debug [create a launch.json file](command:{0}).", DEBUG_CONFIGURE_COMMAND_ID), + "To customize Run and Debug [create a launch.json file](command:{0}).", `${DEBUG_CONFIGURE_COMMAND_ID}?${encodeURIComponent(JSON.stringify([{ addNew: true }]))}`), when: ContextKeyExpr.and(CONTEXT_DEBUGGERS_AVAILABLE, WorkbenchStateContext.notEqualsTo('empty')), group: ViewContentGroups.Debug }); diff --git a/src/vs/workbench/contrib/debug/common/debug.ts b/src/vs/workbench/contrib/debug/common/debug.ts index 72edcb354cfd9..9d26e02c3f445 100644 --- a/src/vs/workbench/contrib/debug/common/debug.ts +++ b/src/vs/workbench/contrib/debug/common/debug.ts @@ -1018,7 +1018,8 @@ export interface IConfigurationManager { onDidChangeConfigurationProviders: Event; hasDebugConfigurationProvider(debugType: string, triggerKind?: DebugConfigurationProviderTriggerKind): boolean; - getDynamicProviders(): Promise<{ label: string; type: string; pick: () => Promise<{ launch: ILaunch; config: IConfig } | undefined> }[]>; + getDynamicProviders(): Promise<{ label: string; type: string; pick: () => Promise<{ launch: ILaunch; config: IConfig; label: string } | undefined> }[]>; + getDynamicConfigurationsByType(type: string, token?: CancellationToken): Promise<{ launch: ILaunch; config: IConfig; label: string }[]>; registerDebugConfigurationProvider(debugConfigurationProvider: IDebugConfigurationProvider): IDisposable; unregisterDebugConfigurationProvider(debugConfigurationProvider: IDebugConfigurationProvider): void; @@ -1049,11 +1050,20 @@ export interface IAdapterManager { substituteVariables(debugType: string, folder: IWorkspaceFolder | undefined, config: IConfig): Promise; runInTerminal(debugType: string, args: DebugProtocol.RunInTerminalRequestArguments, sessionId: string): Promise; getEnabledDebugger(type: string): (IDebugger & IDebuggerMetadata) | undefined; - guessDebugger(gettingConfigurations: boolean): Promise<(IDebugger & IDebuggerMetadata) | undefined>; + guessDebugger(gettingConfigurations: boolean): Promise; get onDidDebuggersExtPointRead(): Event; } +export interface IGuessedDebugger { + debugger: IDebugger; + withConfig?: { + label: string; + launch: ILaunch; + config: IConfig; + }; +} + export interface ILaunch { /** diff --git a/src/vs/workbench/contrib/debug/common/debugStorage.ts b/src/vs/workbench/contrib/debug/common/debugStorage.ts index f4b4dd6a7a6a7..5c2a8f485bab4 100644 --- a/src/vs/workbench/contrib/debug/common/debugStorage.ts +++ b/src/vs/workbench/contrib/debug/common/debugStorage.ts @@ -12,6 +12,7 @@ import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uri import { IDebugModel, IEvaluate, IExpression } from './debug.js'; import { Breakpoint, DataBreakpoint, ExceptionBreakpoint, Expression, FunctionBreakpoint } from './debugModel.js'; import { ITextFileService } from '../../../services/textfile/common/textfiles.js'; +import { mapValues } from '../../../../base/common/objects.js'; const DEBUG_BREAKPOINTS_KEY = 'debug.breakpoint'; const DEBUG_FUNCTION_BREAKPOINTS_KEY = 'debug.functionbreakpoint'; @@ -21,6 +22,11 @@ const DEBUG_WATCH_EXPRESSIONS_KEY = 'debug.watchexpressions'; const DEBUG_CHOSEN_ENVIRONMENTS_KEY = 'debug.chosenenvironment'; const DEBUG_UX_STATE_KEY = 'debug.uxstate'; +export interface IChosenEnvironment { + type: string; + dynamicLabel?: string; +} + export class DebugStorage extends Disposable { public readonly breakpoints = observableValue(this, this.loadBreakpoints()); public readonly functionBreakpoints = observableValue(this, this.loadFunctionBreakpoints()); @@ -118,11 +124,13 @@ export class DebugStorage extends Disposable { return result || []; } - loadChosenEnvironments(): { [key: string]: string } { - return JSON.parse(this.storageService.get(DEBUG_CHOSEN_ENVIRONMENTS_KEY, StorageScope.WORKSPACE, '{}')); + loadChosenEnvironments(): Record { + const obj = JSON.parse(this.storageService.get(DEBUG_CHOSEN_ENVIRONMENTS_KEY, StorageScope.WORKSPACE, '{}')); + // back compat from when this was a string map: + return mapValues(obj, (value): IChosenEnvironment => typeof value === 'string' ? { type: value } : value); } - storeChosenEnvironments(environments: { [key: string]: string }): void { + storeChosenEnvironments(environments: Record): void { this.storageService.store(DEBUG_CHOSEN_ENVIRONMENTS_KEY, JSON.stringify(environments), StorageScope.WORKSPACE, StorageTarget.MACHINE); } From 3b6e0ac43160faef6cc118d24301fda8b406f4d0 Mon Sep 17 00:00:00 2001 From: Joyce Er Date: Fri, 22 Nov 2024 12:59:12 -0800 Subject: [PATCH 073/119] fix: don't include slash command disambiguation twice --- .../chat/browser/chatParticipantContributions.ts | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatParticipantContributions.ts b/src/vs/workbench/contrib/chat/browser/chatParticipantContributions.ts index 9eefdcd1709bb..cb813616ec2f0 100644 --- a/src/vs/workbench/contrib/chat/browser/chatParticipantContributions.ts +++ b/src/vs/workbench/contrib/chat/browser/chatParticipantContributions.ts @@ -212,26 +212,17 @@ export class ChatExtensionPointHandler implements IWorkbenchContribution { continue; } - const participantsAndCommandsDisambiguation: { + const participantsDisambiguation: { category: string; description: string; examples: string[]; }[] = []; if (providerDescriptor.disambiguation?.length) { - participantsAndCommandsDisambiguation.push(...providerDescriptor.disambiguation.map((d) => ({ + participantsDisambiguation.push(...providerDescriptor.disambiguation.map((d) => ({ ...d, category: d.category ?? d.categoryName }))); } - if (providerDescriptor.commands) { - for (const command of providerDescriptor.commands) { - if (command.disambiguation?.length) { - participantsAndCommandsDisambiguation.push(...command.disambiguation.map((d) => ({ - ...d, category: d.category ?? d.categoryName - }))); - } - } - } try { const store = new DisposableStore(); @@ -256,7 +247,7 @@ export class ChatExtensionPointHandler implements IWorkbenchContribution { providerDescriptor.locations.map(ChatAgentLocation.fromRaw) : [ChatAgentLocation.Panel], slashCommands: providerDescriptor.commands ?? [], - disambiguation: coalesce(participantsAndCommandsDisambiguation.flat()), + disambiguation: coalesce(participantsDisambiguation.flat()), } satisfies IChatAgentData)); this._participantRegistrationDisposables.set( From f3a2f08b175f9e77c5142a91ba8938dd8c82e3e8 Mon Sep 17 00:00:00 2001 From: Joyce Er Date: Fri, 22 Nov 2024 13:06:18 -0800 Subject: [PATCH 074/119] fix: disambiguation request should receive participants for that location only --- src/vs/workbench/contrib/chat/common/chatAgents.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/chat/common/chatAgents.ts b/src/vs/workbench/contrib/chat/common/chatAgents.ts index b8c595115e58f..e8d88c925987d 100644 --- a/src/vs/workbench/contrib/chat/common/chatAgents.ts +++ b/src/vs/workbench/contrib/chat/common/chatAgents.ts @@ -502,9 +502,11 @@ export class ChatAgentService extends Disposable implements IChatAgentService { } const participants = this.getAgents().reduce((acc, a) => { - acc.push({ participant: a.id, disambiguation: a.disambiguation ?? [] }); - for (const command of a.slashCommands) { - acc.push({ participant: a.id, command: command.name, disambiguation: command.disambiguation ?? [] }); + if (a.locations.includes(options.location)) { + acc.push({ participant: a.id, disambiguation: a.disambiguation ?? [] }); + for (const command of a.slashCommands) { + acc.push({ participant: a.id, command: command.name, disambiguation: command.disambiguation ?? [] }); + } } return acc; }, []); From c7c53b668cbd2becf5accd877d1d90803b8d9ca5 Mon Sep 17 00:00:00 2001 From: Christof Marti Date: Fri, 22 Nov 2024 22:18:35 +0100 Subject: [PATCH 075/119] Fix kerberos import (github/copilot#13764) --- src/vs/workbench/api/node/proxyResolver.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/api/node/proxyResolver.ts b/src/vs/workbench/api/node/proxyResolver.ts index 8378d4fb98a03..930d8bd5b5954 100644 --- a/src/vs/workbench/api/node/proxyResolver.ts +++ b/src/vs/workbench/api/node/proxyResolver.ts @@ -480,7 +480,8 @@ async function lookupProxyAuthorization( state.kerberosRequested = true; try { - const kerberos = await import('kerberos'); + const importKerberos = await import('kerberos'); + const kerberos = importKerberos.default || importKerberos; const url = new URL(proxyURL); const spn = configProvider.getConfiguration('http').get('proxyKerberosServicePrincipal') || (process.platform === 'win32' ? `HTTP/${url.hostname}` : `HTTP@${url.hostname}`); From 595e0fa1ef400a1798df32c840b849cc84ed429c Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt Date: Fri, 22 Nov 2024 13:35:49 -0800 Subject: [PATCH 076/119] No need for memento hack (#234450) MSAL node made `clearCache` synchronous :tada: so we can safely depend on it for clearing the cache. > Context: The default behavior of MSAL's internal cache is that it is a union with what's in the persistant cache (secret storage) but what _we_ want is that secret storage is the source of truth, so every time we receive an update to secret storage, we clear the in-memory cache to get the data from the persistant cache. Also bumps msal-node-extensions while we're at it. --- .../package-lock.json | 40 +++++++------------ .../microsoft-authentication/package.json | 4 +- .../src/node/cachedPublicClientApplication.ts | 22 ++-------- 3 files changed, 21 insertions(+), 45 deletions(-) diff --git a/extensions/microsoft-authentication/package-lock.json b/extensions/microsoft-authentication/package-lock.json index c52e019da9a96..9f69f13972cb2 100644 --- a/extensions/microsoft-authentication/package-lock.json +++ b/extensions/microsoft-authentication/package-lock.json @@ -10,8 +10,8 @@ "license": "MIT", "dependencies": { "@azure/ms-rest-azure-env": "^2.0.0", - "@azure/msal-node": "^2.13.1", - "@azure/msal-node-extensions": "^1.3.0", + "@azure/msal-node": "^2.16.2", + "@azure/msal-node-extensions": "^1.5.0", "@vscode/extension-telemetry": "^0.9.0", "keytar": "file:./packageMocks/keytar", "vscode-tas-client": "^0.1.84" @@ -33,19 +33,21 @@ "integrity": "sha512-dG76W7ElfLi+fbTjnZVGj+M9e0BIEJmRxU6fHaUQ12bZBe8EJKYb2GV50YWNaP2uJiVQ5+7nXEVj1VN1UQtaEw==" }, "node_modules/@azure/msal-common": { - "version": "14.14.2", - "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-14.14.2.tgz", - "integrity": "sha512-XV0P5kSNwDwCA/SjIxTe9mEAsKB0NqGNSuaVrkCCE2lAyBr/D6YtD80Vkdp4tjWnPFwjzkwldjr1xU/facOJog==", + "version": "14.16.0", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-14.16.0.tgz", + "integrity": "sha512-1KOZj9IpcDSwpNiQNjt0jDYZpQvNZay7QAEi/5DLubay40iGYtLzya/jbjRPLyOTZhEKyL1MzPuw2HqBCjceYA==", + "license": "MIT", "engines": { "node": ">=0.8.0" } }, "node_modules/@azure/msal-node": { - "version": "2.13.1", - "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-2.13.1.tgz", - "integrity": "sha512-sijfzPNorKt6+9g1/miHwhj6Iapff4mPQx1azmmZExgzUROqWTM1o3ACyxDja0g47VpowFy/sxTM/WsuCyXTiw==", + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-2.16.2.tgz", + "integrity": "sha512-An7l1hEr0w1HMMh1LU+rtDtqL7/jw74ORlc9Wnh06v7TU/xpG39/Zdr1ZJu3QpjUfKJ+E0/OXMW8DRSWTlh7qQ==", + "license": "MIT", "dependencies": { - "@azure/msal-common": "14.14.2", + "@azure/msal-common": "14.16.0", "jsonwebtoken": "^9.0.0", "uuid": "^8.3.0" }, @@ -54,13 +56,13 @@ } }, "node_modules/@azure/msal-node-extensions": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@azure/msal-node-extensions/-/msal-node-extensions-1.3.0.tgz", - "integrity": "sha512-7rXN+9hDm3NncIfNnMyoFtsnz2AlUtmK5rsY3P+fhhbH+GOk0W5Y1BASvAB6RCcKdO+qSIK3ZA6VHQYy4iS/1w==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@azure/msal-node-extensions/-/msal-node-extensions-1.5.0.tgz", + "integrity": "sha512-UfEyh2xmJHKH64zPS/SbN1bd9adV4ZWGp1j2OSwIuhVraqpUXyXZ1LpDpiUqg/peTgLLtx20qrHOzYT0kKzmxQ==", "hasInstallScript": true, "license": "MIT", "dependencies": { - "@azure/msal-common": "14.15.0", + "@azure/msal-common": "14.16.0", "@azure/msal-node-runtime": "^0.17.1", "keytar": "^7.8.0" }, @@ -68,18 +70,6 @@ "node": ">=16" } }, - "node_modules/@azure/msal-node-extensions/node_modules/@azure/msal-common": { - "version": "14.15.0", - "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-14.15.0.tgz", - "integrity": "sha512-ImAQHxmpMneJ/4S8BRFhjt1MZ3bppmpRPYYNyzeQPeFN288YKbb8TmmISQEbtfkQ1BPASvYZU5doIZOPBAqENQ==", - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@azure/msal-node-extensions/packageMocks/keytar": { - "extraneous": true - }, "node_modules/@azure/msal-node-runtime": { "version": "0.17.1", "resolved": "https://registry.npmjs.org/@azure/msal-node-runtime/-/msal-node-runtime-0.17.1.tgz", diff --git a/extensions/microsoft-authentication/package.json b/extensions/microsoft-authentication/package.json index 6751caa5336ec..01af890c37ab6 100644 --- a/extensions/microsoft-authentication/package.json +++ b/extensions/microsoft-authentication/package.json @@ -140,8 +140,8 @@ }, "dependencies": { "@azure/ms-rest-azure-env": "^2.0.0", - "@azure/msal-node": "^2.13.1", - "@azure/msal-node-extensions": "^1.3.0", + "@azure/msal-node": "^2.16.2", + "@azure/msal-node-extensions": "^1.5.0", "@vscode/extension-telemetry": "^0.9.0", "keytar": "file:./packageMocks/keytar", "vscode-tas-client": "^0.1.84" diff --git a/extensions/microsoft-authentication/src/node/cachedPublicClientApplication.ts b/extensions/microsoft-authentication/src/node/cachedPublicClientApplication.ts index 27c2de942c180..bc6a392d16cb0 100644 --- a/extensions/microsoft-authentication/src/node/cachedPublicClientApplication.ts +++ b/extensions/microsoft-authentication/src/node/cachedPublicClientApplication.ts @@ -45,14 +45,6 @@ export class CachedPublicClientApplication implements ICachedPublicClientApplica }; private readonly _isBrokerAvailable = this._config.broker?.nativeBrokerPlugin?.isBrokerAvailable ?? false; - /** - * We keep track of the last time an account was removed so we can recreate the PCA if we detect that an account was removed. - * This is due to MSAL-node not providing a way to detect when an account is removed from the cache. An internal issue has been - * filed to track this. If MSAL-node ever provides a way to detect this or handle this better in the Persistant Cache Plugin, - * we can remove this logic. - */ - private _lastCreated: Date; - //#region Events private readonly _onDidAccountsChangeEmitter = new EventEmitter<{ added: AccountInfo[]; changed: AccountInfo[]; deleted: AccountInfo[] }>; @@ -71,8 +63,9 @@ export class CachedPublicClientApplication implements ICachedPublicClientApplica private readonly _secretStorage: SecretStorage, private readonly _logger: LogOutputChannel ) { + // TODO:@TylerLeonhardt clean up old use of memento. Remove this in an iteration + this._globalMemento.update(`lastRemoval:${this._clientId}:${this._authority}`, undefined); this._pca = new PublicClientApplication(this._config); - this._lastCreated = new Date(); this._disposable = Disposable.from( this._registerOnSecretStorageChanged(), this._onDidAccountsChangeEmitter, @@ -147,7 +140,6 @@ export class CachedPublicClientApplication implements ICachedPublicClientApplica } removeAccount(account: AccountInfo): Promise { - this._globalMemento.update(`lastRemoval:${this._clientId}:${this._authority}`, new Date()); if (this._isBrokerAvailable) { return this._accountAccess.setAllowedAccess(account, false); } @@ -185,14 +177,8 @@ export class CachedPublicClientApplication implements ICachedPublicClientApplica private async _update() { const before = this._accounts; this._logger.debug(`[update] [${this._clientId}] [${this._authority}] CachedPublicClientApplication update before: ${before.length}`); - // Dates are stored as strings in the memento - const lastRemovalDate = this._globalMemento.get(`lastRemoval:${this._clientId}:${this._authority}`); - if (lastRemovalDate && this._lastCreated && Date.parse(lastRemovalDate) > this._lastCreated.getTime()) { - this._logger.debug(`[update] [${this._clientId}] [${this._authority}] CachedPublicClientApplication removal detected... recreating PCA...`); - this._pca = new PublicClientApplication(this._config); - this._lastCreated = new Date(); - } - + // Clear in-memory cache so we know we're getting account data from the SecretStorage + this._pca.clearCache(); let after = await this._pca.getAllAccounts(); if (this._isBrokerAvailable) { after = after.filter(a => this._accountAccess.isAllowedAccess(a)); From 13e5c280b693d008f559c895a8607c8b73b8edbb Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt Date: Fri, 22 Nov 2024 15:39:17 -0800 Subject: [PATCH 077/119] Remove msal-node-extensions workaround (#234456) We needed this workaround because MSAL was always trying to require a native module we never use. I sent a PR to MSAL to rework their behavior and that has now been released and we pulled that in in https://github.com/microsoft/vscode/pull/234450 With the updated msal-node-extensions library, we no longer need to do this webpack logic. --- .../extension.webpack.config.js | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/extensions/microsoft-authentication/extension.webpack.config.js b/extensions/microsoft-authentication/extension.webpack.config.js index a85bb7b9b77b7..395c011b1dba1 100644 --- a/extensions/microsoft-authentication/extension.webpack.config.js +++ b/extensions/microsoft-authentication/extension.webpack.config.js @@ -10,7 +10,6 @@ const withDefaults = require('../shared.webpack.config'); const CopyWebpackPlugin = require('copy-webpack-plugin'); const path = require('path'); -const { NormalModuleReplacementPlugin } = require('webpack'); const isWindows = process.platform === 'win32'; @@ -42,13 +41,6 @@ module.exports = withDefaults({ noErrorOnMissing: !isWindows } ] - }), - // We don't use the feature that uses Dpapi, so we can just replace it with a mock. - // This is a bit of a hack, but it's the easiest way to do it. Really, msal should - // handle when this native node module is not available. - new NormalModuleReplacementPlugin( - /\.\.\/Dpapi\.mjs/, - path.resolve(__dirname, 'packageMocks', 'dpapi', 'dpapi.js') - ) + }) ] }); From 06b50278ec193f5bd0486fd17b7797611040363f Mon Sep 17 00:00:00 2001 From: Bhavya U Date: Fri, 22 Nov 2024 15:52:06 -0800 Subject: [PATCH 078/119] fix: update localization for walkthrough page title in GettingStartedInput (#234457) --- .../welcomeGettingStarted/browser/gettingStartedInput.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedInput.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedInput.ts index 0f699052cc089..7375366e5c695 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedInput.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedInput.ts @@ -75,7 +75,7 @@ export class GettingStartedInput extends EditorInput { } override getName() { - return this.walkthroughPageTitle ? localize('walkthroughPageTitle', 'Walkthrough: ') + this.walkthroughPageTitle : localize('getStarted', "Welcome"); + return this.walkthroughPageTitle ? localize('walkthroughPageTitle', 'Walkthrough: {0}', this.walkthroughPageTitle) : localize('getStarted', "Welcome"); } get selectedCategory() { From dfffb4475b446aa0b5bb48fe0cead680486937c1 Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt Date: Fri, 22 Nov 2024 16:17:07 -0800 Subject: [PATCH 079/119] Update logging & delete dead code (#234458) * Update logging values so the logs aren't so noisy * Delete a bunch of dead async code --- .../src/common/async.ts | 315 +----------------- .../src/common/loggerOptions.ts | 12 +- .../microsoft-authentication/src/extension.ts | 10 +- .../src/node/cachedPublicClientApplication.ts | 2 +- 4 files changed, 15 insertions(+), 324 deletions(-) diff --git a/extensions/microsoft-authentication/src/common/async.ts b/extensions/microsoft-authentication/src/common/async.ts index 094861518fc61..9eebbb24f65ad 100644 --- a/extensions/microsoft-authentication/src/common/async.ts +++ b/extensions/microsoft-authentication/src/common/async.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { CancellationError, CancellationToken, Disposable, Event, EventEmitter } from 'vscode'; +import { CancellationError, CancellationToken, Disposable, Event } from 'vscode'; /** * Can be passed into the Delayed to defer using a microtask @@ -67,12 +67,6 @@ export function raceCancellationError(promise: Promise, token: Cancellatio }); } -export class TimeoutError extends Error { - constructor() { - super('Timed out'); - } -} - export function raceTimeoutError(promise: Promise, timeout: number): Promise { return new Promise((resolve, reject) => { const ref = setTimeout(() => { @@ -86,114 +80,6 @@ export function raceCancellationAndTimeoutError(promise: Promise, token: C return raceCancellationError(raceTimeoutError(promise, timeout), token); } -interface ILimitedTaskFactory { - factory: () => Promise; - c: (value: T | Promise) => void; - e: (error?: unknown) => void; -} - -export interface ILimiter { - - readonly size: number; - - queue(factory: () => Promise): Promise; - - clear(): void; -} - -/** - * A helper to queue N promises and run them all with a max degree of parallelism. The helper - * ensures that at any time no more than M promises are running at the same time. - */ -export class Limiter implements ILimiter { - - private _size = 0; - private _isDisposed = false; - private runningPromises: number; - private readonly maxDegreeOfParalellism: number; - private readonly outstandingPromises: ILimitedTaskFactory[]; - private readonly _onDrained: EventEmitter; - - constructor(maxDegreeOfParalellism: number) { - this.maxDegreeOfParalellism = maxDegreeOfParalellism; - this.outstandingPromises = []; - this.runningPromises = 0; - this._onDrained = new EventEmitter(); - } - - /** - * - * @returns A promise that resolved when all work is done (onDrained) or when - * there is nothing to do - */ - whenIdle(): Promise { - return this.size > 0 - ? toPromise(this.onDrained) - : Promise.resolve(); - } - - get onDrained(): Event { - return this._onDrained.event; - } - - get size(): number { - return this._size; - } - - queue(factory: () => Promise): Promise { - if (this._isDisposed) { - throw new Error('Object has been disposed'); - } - this._size++; - - return new Promise((c, e) => { - this.outstandingPromises.push({ factory, c, e }); - this.consume(); - }); - } - - private consume(): void { - while (this.outstandingPromises.length && this.runningPromises < this.maxDegreeOfParalellism) { - const iLimitedTask = this.outstandingPromises.shift()!; - this.runningPromises++; - - const promise = iLimitedTask.factory(); - promise.then(iLimitedTask.c, iLimitedTask.e); - promise.then(() => this.consumed(), () => this.consumed()); - } - } - - private consumed(): void { - if (this._isDisposed) { - return; - } - this.runningPromises--; - if (--this._size === 0) { - this._onDrained.fire(); - } - - if (this.outstandingPromises.length > 0) { - this.consume(); - } - } - - clear(): void { - if (this._isDisposed) { - throw new Error('Object has been disposed'); - } - this.outstandingPromises.length = 0; - this._size = this.runningPromises; - } - - dispose(): void { - this._isDisposed = true; - this.outstandingPromises.length = 0; // stop further processing - this._size = 0; - this._onDrained.dispose(); - } -} - - interface IScheduledLater extends Disposable { isTriggered(): boolean; } @@ -320,143 +206,6 @@ export class Delayer implements Disposable { } } -/** - * A helper to prevent accumulation of sequential async tasks. - * - * Imagine a mail man with the sole task of delivering letters. As soon as - * a letter submitted for delivery, he drives to the destination, delivers it - * and returns to his base. Imagine that during the trip, N more letters were submitted. - * When the mail man returns, he picks those N letters and delivers them all in a - * single trip. Even though N+1 submissions occurred, only 2 deliveries were made. - * - * The throttler implements this via the queue() method, by providing it a task - * factory. Following the example: - * - * const throttler = new Throttler(); - * const letters = []; - * - * function deliver() { - * const lettersToDeliver = letters; - * letters = []; - * return makeTheTrip(lettersToDeliver); - * } - * - * function onLetterReceived(l) { - * letters.push(l); - * throttler.queue(deliver); - * } - */ -export class Throttler implements Disposable { - - private activePromise: Promise | null; - private queuedPromise: Promise | null; - private queuedPromiseFactory: (() => Promise) | null; - - private isDisposed = false; - - constructor() { - this.activePromise = null; - this.queuedPromise = null; - this.queuedPromiseFactory = null; - } - - queue(promiseFactory: () => Promise): Promise { - if (this.isDisposed) { - return Promise.reject(new Error('Throttler is disposed')); - } - - if (this.activePromise) { - this.queuedPromiseFactory = promiseFactory; - - if (!this.queuedPromise) { - const onComplete = () => { - this.queuedPromise = null; - - if (this.isDisposed) { - return; - } - - const result = this.queue(this.queuedPromiseFactory!); - this.queuedPromiseFactory = null; - - return result; - }; - - this.queuedPromise = new Promise(resolve => { - this.activePromise!.then(onComplete, onComplete).then(resolve); - }); - } - - return new Promise((resolve, reject) => { - this.queuedPromise!.then(resolve, reject); - }); - } - - this.activePromise = promiseFactory(); - - return new Promise((resolve, reject) => { - this.activePromise!.then((result: T) => { - this.activePromise = null; - resolve(result); - }, (err: unknown) => { - this.activePromise = null; - reject(err); - }); - }); - } - - dispose(): void { - this.isDisposed = true; - } -} - -/** - * A helper to delay execution of a task that is being requested often, while - * preventing accumulation of consecutive executions, while the task runs. - * - * The mail man is clever and waits for a certain amount of time, before going - * out to deliver letters. While the mail man is going out, more letters arrive - * and can only be delivered once he is back. Once he is back the mail man will - * do one more trip to deliver the letters that have accumulated while he was out. - */ -export class ThrottledDelayer { - - private delayer: Delayer>; - private throttler: Throttler; - - constructor(defaultDelay: number) { - this.delayer = new Delayer(defaultDelay); - this.throttler = new Throttler(); - } - - trigger(promiseFactory: () => Promise, delay?: number): Promise { - return this.delayer.trigger(() => this.throttler.queue(promiseFactory), delay) as unknown as Promise; - } - - isTriggered(): boolean { - return this.delayer.isTriggered(); - } - - cancel(): void { - this.delayer.cancel(); - } - - dispose(): void { - this.delayer.dispose(); - this.throttler.dispose(); - } -} - -/** - * A queue is handles one promise at a time and guarantees that at any time only one promise is executing. - */ -export class Queue extends Limiter { - - constructor() { - super(1); - } -} - /** * Given an event, returns another event which only fires once. * @@ -493,65 +242,3 @@ export function once(event: Event): Event { export function toPromise(event: Event): Promise { return new Promise(resolve => once(event)(resolve)); } - -export type ValueCallback = (value: T | Promise) => void; - -const enum DeferredOutcome { - Resolved, - Rejected -} - -/** - * Creates a promise whose resolution or rejection can be controlled imperatively. - */ -export class DeferredPromise { - - private completeCallback!: ValueCallback; - private errorCallback!: (err: unknown) => void; - private outcome?: { outcome: DeferredOutcome.Rejected; value: any } | { outcome: DeferredOutcome.Resolved; value: T }; - - public get isRejected() { - return this.outcome?.outcome === DeferredOutcome.Rejected; - } - - public get isResolved() { - return this.outcome?.outcome === DeferredOutcome.Resolved; - } - - public get isSettled() { - return !!this.outcome; - } - - public get value() { - return this.outcome?.outcome === DeferredOutcome.Resolved ? this.outcome?.value : undefined; - } - - public readonly p: Promise; - - constructor() { - this.p = new Promise((c, e) => { - this.completeCallback = c; - this.errorCallback = e; - }); - } - - public complete(value: T) { - return new Promise(resolve => { - this.completeCallback(value); - this.outcome = { outcome: DeferredOutcome.Resolved, value }; - resolve(); - }); - } - - public error(err: unknown) { - return new Promise(resolve => { - this.errorCallback(err); - this.outcome = { outcome: DeferredOutcome.Rejected, value: err }; - resolve(); - }); - } - - public cancel() { - return this.error(new CancellationError()); - } -} diff --git a/extensions/microsoft-authentication/src/common/loggerOptions.ts b/extensions/microsoft-authentication/src/common/loggerOptions.ts index 86443c0281f0a..d572f655f9267 100644 --- a/extensions/microsoft-authentication/src/common/loggerOptions.ts +++ b/extensions/microsoft-authentication/src/common/loggerOptions.ts @@ -17,9 +17,13 @@ export class MsalLoggerOptions { loggerCallback(level: MsalLogLevel, message: string, containsPii: boolean): void { if (containsPii) { + // TODO: Should we still log the message if it contains PII? It's just going to + // an output channel that doesn't leave the machine. + this._output.debug('Skipped logging message because it may contain PII'); return; } + // Log to output channel one level lower than the MSAL log level switch (level) { case MsalLogLevel.Error: this._output.error(message); @@ -28,16 +32,16 @@ export class MsalLoggerOptions { this._output.warn(message); return; case MsalLogLevel.Info: - this._output.info(message); + this._output.debug(message); return; case MsalLogLevel.Verbose: - this._output.debug(message); + this._output.trace(message); return; case MsalLogLevel.Trace: - this._output.trace(message); + // Do not log trace messages return; default: - this._output.info(message); + this._output.debug(message); return; } } diff --git a/extensions/microsoft-authentication/src/extension.ts b/extensions/microsoft-authentication/src/extension.ts index 333a183f4b7c7..00f24e6d25cd4 100644 --- a/extensions/microsoft-authentication/src/extension.ts +++ b/extensions/microsoft-authentication/src/extension.ts @@ -15,26 +15,26 @@ function shouldUseMsal(expService: IExperimentationService): boolean { // First check if there is a setting value to allow user to override the default const inspect = workspace.getConfiguration('microsoft-authentication').inspect<'msal' | 'classic'>('implementation'); if (inspect?.workspaceFolderValue !== undefined) { - Logger.debug(`Acquired MSAL enablement value from 'workspaceFolderValue'. Value: ${inspect.workspaceFolderValue}`); + Logger.info(`Acquired MSAL enablement value from 'workspaceFolderValue'. Value: ${inspect.workspaceFolderValue}`); return inspect.workspaceFolderValue === 'msal'; } if (inspect?.workspaceValue !== undefined) { - Logger.debug(`Acquired MSAL enablement value from 'workspaceValue'. Value: ${inspect.workspaceValue}`); + Logger.info(`Acquired MSAL enablement value from 'workspaceValue'. Value: ${inspect.workspaceValue}`); return inspect.workspaceValue === 'msal'; } if (inspect?.globalValue !== undefined) { - Logger.debug(`Acquired MSAL enablement value from 'globalValue'. Value: ${inspect.globalValue}`); + Logger.info(`Acquired MSAL enablement value from 'globalValue'. Value: ${inspect.globalValue}`); return inspect.globalValue === 'msal'; } // Then check if the experiment value const expValue = expService.getTreatmentVariable('vscode', 'microsoft.useMsal'); if (expValue !== undefined) { - Logger.debug(`Acquired MSAL enablement value from 'exp'. Value: ${expValue}`); + Logger.info(`Acquired MSAL enablement value from 'exp'. Value: ${expValue}`); return expValue; } - Logger.debug('Acquired MSAL enablement value from default. Value: false'); + Logger.info('Acquired MSAL enablement value from default. Value: true'); // If no setting or experiment value is found, default to false return true; } diff --git a/extensions/microsoft-authentication/src/node/cachedPublicClientApplication.ts b/extensions/microsoft-authentication/src/node/cachedPublicClientApplication.ts index bc6a392d16cb0..dc97fb3a352a2 100644 --- a/extensions/microsoft-authentication/src/node/cachedPublicClientApplication.ts +++ b/extensions/microsoft-authentication/src/node/cachedPublicClientApplication.ts @@ -33,7 +33,7 @@ export class CachedPublicClientApplication implements ICachedPublicClientApplica loggerOptions: { correlationId: `${this._clientId}] [${this._authority}`, loggerCallback: (level, message, containsPii) => this._loggerOptions.loggerCallback(level, message, containsPii), - logLevel: LogLevel.Info + logLevel: LogLevel.Trace } }, broker: { From c3ddd541b64f31c2535f185cd75185fe7191d2bf Mon Sep 17 00:00:00 2001 From: Joyce Er Date: Fri, 22 Nov 2024 18:23:00 -0800 Subject: [PATCH 080/119] fix: add tooltip for chat collapsed code blocks (#234460) --- .../chat/browser/chatContentParts/chatMarkdownContentPart.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts index 4cd638dedf1a0..d4b7425654d53 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts @@ -388,6 +388,9 @@ class CollapsedCodeBlock extends Disposable { } labelAdded.textContent = `+${addedLines}`; labelRemoved.textContent = `-${removedLines}`; + const insertionsFragment = addedLines === 1 ? localize('chat.codeblock.insertions.one', "{0} insertion") : localize('chat.codeblock.insertions', "{0} insertions", addedLines); + const deletionsFragment = removedLines === 1 ? localize('chat.codeblock.deletions.one', "{0} deletion") : localize('chat.codeblock.deletions', "{0} deletions", removedLines); + this.element.ariaLabel = this.element.title = localize('summary', 'Edited {0}, {1}, {2}', iconText, insertionsFragment, deletionsFragment); } } })); From ead1822ee853ff10267f495d12128883472629d7 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Fri, 22 Nov 2024 22:33:44 -0500 Subject: [PATCH 081/119] fix terminal suggest dir bug (#234462) fix dir bug --- .../suggest/browser/terminalCompletionService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts index bee024edb19eb..ee6b3a6bb354e 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts @@ -227,7 +227,7 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo continue; } - const label = prefix + stat.resource.fsPath.replace(cwd.fsPath, ''); + const label = prefix + stat.resource.fsPath.replace(dir.fsPath, ''); resourceCompletions.push({ label, kind, From 938ff2d3984a5118f7447b97e11b172b04d56e78 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Sat, 23 Nov 2024 10:26:07 +0100 Subject: [PATCH 082/119] chat - rewrite setup welcome (#234413) --- package.json | 2 +- src/vs/base/browser/ui/toggle/toggle.ts | 4 + src/vs/base/common/product.ts | 8 +- .../browser/markdownRenderer.ts | 2 +- .../parts/editor/editorGroupWatermark.ts | 4 +- .../chat/browser/actions/chatActions.ts | 4 +- .../browser/chatParticipantContributions.ts | 2 - .../chat/browser/chatSetup.contribution.ts | 679 ++++++++++-------- .../chat/browser/media/chatViewSetup.css | 25 + .../chat/browser/media/chatViewWelcome.css | 6 - .../viewsWelcome/chatViewWelcomeController.ts | 65 +- .../browser/viewsWelcome/chatViewsWelcome.ts | 4 +- .../contrib/chat/common/chatContextKeys.ts | 5 - 13 files changed, 473 insertions(+), 337 deletions(-) create mode 100644 src/vs/workbench/contrib/chat/browser/media/chatViewSetup.css diff --git a/package.json b/package.json index 6b8c2b3f15945..60c69be0d2518 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.96.0", - "distro": "83d6d153cc14e1f45b706cdc00383e54f25051bf", + "distro": "0f524a5cfa305bf4a8cc06ca7fd2d4363ec8c7c1", "author": { "name": "Microsoft Corporation" }, diff --git a/src/vs/base/browser/ui/toggle/toggle.ts b/src/vs/base/browser/ui/toggle/toggle.ts index a232e531e0258..a6c913662f93a 100644 --- a/src/vs/base/browser/ui/toggle/toggle.ts +++ b/src/vs/base/browser/ui/toggle/toggle.ts @@ -266,6 +266,10 @@ export class Checkbox extends Widget { return this.checkbox.checked; } + get enabled(): boolean { + return this.checkbox.enabled; + } + set checked(newIsChecked: boolean) { this.checkbox.checked = newIsChecked; diff --git a/src/vs/base/common/product.ts b/src/vs/base/common/product.ts index 90b650e29539b..dbc89e5ddcc90 100644 --- a/src/vs/base/common/product.ts +++ b/src/vs/base/common/product.ts @@ -313,12 +313,12 @@ export interface IDefaultChatAgent { readonly documentationUrl: string; readonly privacyStatementUrl: string; readonly collectionDocumentationUrl: string; + readonly skusDocumentationUrl: string; readonly providerId: string; readonly providerName: string; - readonly providerScopes: string[]; + readonly providerScopes: string[][]; readonly entitlementUrl: string; readonly entitlementChatEnabled: string; - readonly entitlementSkuKey: string; - readonly entitlementSku30DTrialValue: string; - readonly entitlementSkuAlternateUrl: string; + readonly entitlementSkuLimitedUrl: string; + readonly entitlementSkuLimitedEnabled: string; } diff --git a/src/vs/editor/browser/widget/markdownRenderer/browser/markdownRenderer.ts b/src/vs/editor/browser/widget/markdownRenderer/browser/markdownRenderer.ts index 33d1adc99c80f..61ece2ec8e1e1 100644 --- a/src/vs/editor/browser/widget/markdownRenderer/browser/markdownRenderer.ts +++ b/src/vs/editor/browser/widget/markdownRenderer/browser/markdownRenderer.ts @@ -32,7 +32,7 @@ export interface IMarkdownRendererOptions { * Markdown renderer that can render codeblocks with the editor mechanics. This * renderer should always be preferred. */ -export class MarkdownRenderer { +export class MarkdownRenderer implements IDisposable { private static _ttpTokenizer = createTrustedTypesPolicy('tokenizeToString', { createHTML(html: string) { diff --git a/src/vs/workbench/browser/parts/editor/editorGroupWatermark.ts b/src/vs/workbench/browser/parts/editor/editorGroupWatermark.ts index 3c96d9f9ffc09..0c7e37ccc0972 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupWatermark.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupWatermark.ts @@ -38,8 +38,8 @@ const findInFiles: WatermarkEntry = { text: localize('watermark.findInFiles', "F const toggleTerminal: WatermarkEntry = { text: localize({ key: 'watermark.toggleTerminal', comment: ['toggle is a verb here'] }, "Toggle Terminal"), id: 'workbench.action.terminal.toggleTerminal', when: { web: ContextKeyExpr.equals('terminalProcessSupported', true) } }; const startDebugging: WatermarkEntry = { text: localize('watermark.startDebugging', "Start Debugging"), id: 'workbench.action.debug.start', when: { web: ContextKeyExpr.equals('terminalProcessSupported', true) } }; const openSettings: WatermarkEntry = { text: localize('watermark.openSettings', "Open Settings"), id: 'workbench.action.openSettings' }; -const openChat: WatermarkEntry = { text: localize('watermark.openChat', "Open Chat"), id: 'workbench.action.chat.open', when: { native: ContextKeyExpr.equals('chatPanelParticipantRegistered', true), web: ContextKeyExpr.equals('chatPanelParticipantRegistered', true) } }; -const openCopilotEdits: WatermarkEntry = { text: localize('watermark.openCopilotEdits', "Open Copilot Edits"), id: 'workbench.action.chat.openEditSession', when: { native: ContextKeyExpr.equals('chatEditingParticipantRegistered', true), web: ContextKeyExpr.equals('chatEditingParticipantRegistered', true) } }; +const openChat: WatermarkEntry = { text: localize('watermark.openChat', "Open Chat"), id: 'workbench.action.chat.open', when: { native: ContextKeyExpr.equals('chatSetupInstalled', true), web: ContextKeyExpr.equals('chatSetupInstalled', true) } }; +const openCopilotEdits: WatermarkEntry = { text: localize('watermark.openCopilotEdits', "Open Copilot Edits"), id: 'workbench.action.chat.openEditSession', when: { native: ContextKeyExpr.equals('chatSetupInstalled', true), web: ContextKeyExpr.equals('chatSetupInstalled', true) } }; const emptyWindowEntries: WatermarkEntry[] = coalesce([ showCommands, diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts index 59c8caf43aea9..9ee591e5fc2d0 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts @@ -562,8 +562,8 @@ export class ChatCommandCenterRendering implements IWorkbenchContribution { const chatExtensionInstalled = agentService.getAgents().some(agent => agent.isDefault); const primaryAction = instantiationService.createInstance(MenuItemAction, { - id: chatExtensionInstalled ? CHAT_OPEN_ACTION_ID : 'workbench.action.chat.triggerSetup', // TODO@bpasero revisit layering of this action - title: OpenChatGlobalAction.TITLE, + id: chatExtensionInstalled ? CHAT_OPEN_ACTION_ID : 'workbench.action.chat.triggerSetup', + title: chatExtensionInstalled ? OpenChatGlobalAction.TITLE : localize2('triggerChatSetup', "Setup {0}...", defaultChat.name), icon: defaultChat.icon, }, undefined, undefined, undefined, undefined); diff --git a/src/vs/workbench/contrib/chat/browser/chatParticipantContributions.ts b/src/vs/workbench/contrib/chat/browser/chatParticipantContributions.ts index cb813616ec2f0..a7845edbca55a 100644 --- a/src/vs/workbench/contrib/chat/browser/chatParticipantContributions.ts +++ b/src/vs/workbench/contrib/chat/browser/chatParticipantContributions.ts @@ -312,8 +312,6 @@ export class ChatExtensionPointHandler implements IWorkbenchContribution { ctorDescriptor: new SyncDescriptor(ChatViewPane, [{ location: ChatAgentLocation.Panel }]), when: ContextKeyExpr.or( ChatContextKeys.Setup.triggered, - ChatContextKeys.Setup.signingIn, - ChatContextKeys.Setup.installing, ChatContextKeys.Setup.installed, ChatContextKeys.panelParticipantRegistered, ChatContextKeys.extensionInvalid diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts b/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts index 26f4abedbfa52..038ec3f393077 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts @@ -3,10 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import './media/chatViewSetup.css'; import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js'; import { Disposable, toDisposable } from '../../../../base/common/lifecycle.js'; import { ContextKeyExpr, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; -import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; +import { ITelemetryService, TelemetryLevel } from '../../../../platform/telemetry/common/telemetry.js'; import { AuthenticationSession, IAuthenticationService } from '../../../services/authentication/common/authentication.js'; import { IProductService } from '../../../../platform/product/common/productService.js'; import { IExtensionManagementService } from '../../../../platform/extensionManagement/common/extensionManagement.js'; @@ -17,7 +18,6 @@ import { CancellationToken, CancellationTokenSource } from '../../../../base/com import { ChatContextKeys } from '../common/chatContextKeys.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { IRequestContext } from '../../../../base/parts/request/common/request.js'; -import { timeout } from '../../../../base/common/async.js'; import { isCancellationError } from '../../../../base/common/errors.js'; import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js'; import { localize, localize2 } from '../../../../nls.js'; @@ -26,21 +26,23 @@ import { IInstantiationService } from '../../../../platform/instantiation/common import { IWorkbenchLayoutService, Parts } from '../../../services/layout/browser/layoutService.js'; import { IViewsService } from '../../../services/views/common/viewsService.js'; import { IExtensionsWorkbenchService } from '../../extensions/common/extensions.js'; -import { CHAT_CATEGORY } from './actions/chatActions.js'; import { showChatView, ChatViewId } from './chat.js'; -import { IChatAgentService } from '../common/chatAgents.js'; -import { Event } from '../../../../base/common/event.js'; +import { Emitter, Event } from '../../../../base/common/event.js'; import product from '../../../../platform/product/common/product.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { Registry } from '../../../../platform/registry/common/platform.js'; import { IChatViewsWelcomeContributionRegistry, ChatViewsWelcomeExtensions } from './viewsWelcome/chatViewsWelcome.js'; -import { MarkdownString } from '../../../../base/common/htmlContent.js'; import { IViewDescriptorService, ViewContainerLocation } from '../../../common/views.js'; import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; -import { getActiveElement } from '../../../../base/browser/dom.js'; -import { ILogService } from '../../../../platform/log/common/log.js'; +import { $, addDisposableListener, EventType, getActiveElement, setVisibility } from '../../../../base/browser/dom.js'; +import { ILogService, LogLevel } from '../../../../platform/log/common/log.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js'; +import { Checkbox } from '../../../../base/browser/ui/toggle/toggle.js'; +import { defaultButtonStyles, defaultCheckboxStyles } from '../../../../platform/theme/browser/defaultStyles.js'; +import { Button } from '../../../../base/browser/ui/button/button.js'; +import { MarkdownRenderer } from '../../../../editor/browser/widget/markdownRenderer/browser/markdownRenderer.js'; +import { MarkdownString } from '../../../../base/common/htmlContent.js'; const defaultChat = { extensionId: product.defaultChatAgent?.extensionId ?? '', @@ -50,63 +52,39 @@ const defaultChat = { documentationUrl: product.defaultChatAgent?.documentationUrl ?? '', privacyStatementUrl: product.defaultChatAgent?.privacyStatementUrl ?? '', collectionDocumentationUrl: product.defaultChatAgent?.collectionDocumentationUrl ?? '', + skusDocumentationUrl: product.defaultChatAgent?.skusDocumentationUrl ?? '', providerId: product.defaultChatAgent?.providerId ?? '', providerName: product.defaultChatAgent?.providerName ?? '', - providerScopes: product.defaultChatAgent?.providerScopes ?? [], + providerScopes: product.defaultChatAgent?.providerScopes ?? [[]], entitlementUrl: product.defaultChatAgent?.entitlementUrl ?? '', - entitlementSkuKey: product.defaultChatAgent?.entitlementSkuKey ?? '', - entitlementSku30DTrialValue: product.defaultChatAgent?.entitlementSku30DTrialValue ?? '', entitlementChatEnabled: product.defaultChatAgent?.entitlementChatEnabled ?? '', - entitlementSkuAlternateUrl: product.defaultChatAgent?.entitlementSkuAlternateUrl ?? '' -}; - -type ChatSetupEntitlementEnablementClassification = { - entitled: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Flag indicating if the user is chat setup entitled' }; - trial: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Flag indicating if the user is subscribed to chat trial' }; - owner: 'bpasero'; - comment: 'Reporting if the user is chat setup entitled'; -}; - -type ChatSetupEntitlementEnablementEvent = { - entitled: boolean; - trial: boolean; -}; - -type InstallChatClassification = { - owner: 'bpasero'; - comment: 'Provides insight into chat installation.'; - installResult: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the extension was installed successfully, cancelled or failed to install.' }; - signedIn: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the user did sign in prior to installing the extension.' }; -}; -type InstallChatEvent = { - installResult: 'installed' | 'cancelled' | 'failedInstall' | 'failedNotSignedIn'; - signedIn: boolean; + entitlementSkuLimitedUrl: product.defaultChatAgent?.entitlementSkuLimitedUrl ?? '', + entitlementSkuLimitedEnabled: product.defaultChatAgent?.entitlementSkuLimitedEnabled ?? '' }; -interface IChatEntitlement { - readonly chatEnabled?: boolean; - readonly chatSku30DTrial?: boolean; +enum ChatEntitlement { + /** Signed out */ + Unknown = 1, + /** Not yet resolved */ + Unresolved, + /** Signed in and entitled to Sign-up */ + Available, + /** Signed in but not entitled to Sign-up */ + Unavailable } -const UNKNOWN_CHAT_ENTITLEMENT: IChatEntitlement = {}; +//#region Contribution class ChatSetupContribution extends Disposable implements IWorkbenchContribution { - private readonly chatSetupSignedInContextKey = ChatContextKeys.Setup.signedIn.bindTo(this.contextKeyService); - private readonly chatSetupEntitledContextKey = ChatContextKeys.Setup.entitled.bindTo(this.contextKeyService); - private readonly chatSetupState = this.instantiationService.createInstance(ChatSetupState); - - private resolvedEntitlement: IChatEntitlement | undefined = undefined; + private readonly entitlementsResolver = this._register(this.instantiationService.createInstance(ChatSetupEntitlementResolver)); constructor( - @IContextKeyService private readonly contextKeyService: IContextKeyService, - @ITelemetryService private readonly telemetryService: ITelemetryService, - @IAuthenticationService private readonly authenticationService: IAuthenticationService, @IProductService private readonly productService: IProductService, @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, - @IExtensionService private readonly extensionService: IExtensionService, @IInstantiationService private readonly instantiationService: IInstantiationService, + @IExtensionService private readonly extensionService: IExtensionService, ) { super(); @@ -116,87 +94,22 @@ class ChatSetupContribution extends Disposable implements IWorkbenchContribution this.registerChatWelcome(); - this.registerEntitlementListeners(); - this.registerAuthListeners(); - this.checkExtensionInstallation(); } private registerChatWelcome(): void { - const header = localize('setupHeader', "{0} is your AI pair programmer.", defaultChat.name); - const footer = localize({ key: 'setupFooter', comment: ['{Locked="]({0})"}'] }, "By proceeding you agree to our [privacy statement]({0}).", defaultChat.privacyStatementUrl); - - // Setup: Triggered (signed-out) Registry.as(ChatViewsWelcomeExtensions.ChatViewsWelcomeRegistry).register({ title: defaultChat.chatWelcomeTitle, when: ContextKeyExpr.and( ChatContextKeys.Setup.triggered, - ChatContextKeys.Setup.signedIn.negate(), - ChatContextKeys.Setup.signingIn.negate(), - ChatContextKeys.Setup.installing.negate(), ChatContextKeys.Setup.installed.negate() )!, icon: defaultChat.icon, - content: new MarkdownString([ - header, - `[${localize('signInAndSetup', "Sign in to use {0}", defaultChat.name)}](command:${ChatSetupSignInAndInstallChatAction.ID})`, - footer, - `[${localize('learnMore', "Learn More")}](${defaultChat.documentationUrl})`, - ].join('\n\n'), { isTrusted: true }), - }); - - // Setup: Triggered (signed-in) - Registry.as(ChatViewsWelcomeExtensions.ChatViewsWelcomeRegistry).register({ - title: defaultChat.chatWelcomeTitle, - when: ContextKeyExpr.and( - ChatContextKeys.Setup.triggered, - ChatContextKeys.Setup.signedIn, - ChatContextKeys.Setup.signingIn.negate(), - ChatContextKeys.Setup.installing.negate(), - ChatContextKeys.Setup.installed.negate() - )!, - icon: defaultChat.icon, - content: new MarkdownString([ - header, - `[${localize('setup', "Install {0}", defaultChat.name)}](command:${ChatSetupInstallAction.ID})`, - footer, - `[${localize('learnMore', "Learn More")}](${defaultChat.documentationUrl})`, - ].join('\n\n'), { isTrusted: true }) - }); - - // Setup: Signing-in - Registry.as(ChatViewsWelcomeExtensions.ChatViewsWelcomeRegistry).register({ - title: defaultChat.chatWelcomeTitle, - when: ContextKeyExpr.and( - ChatContextKeys.Setup.signingIn, - ChatContextKeys.Setup.installed.negate() - )!, - icon: defaultChat.icon, - disableFirstLinkToButton: true, - content: new MarkdownString([ - header, - localize('setupChatSigningIn', "$(loading~spin) Signing in to {0}...", defaultChat.providerName), - footer, - `[${localize('learnMore', "Learn More")}](${defaultChat.documentationUrl})`, - ].join('\n\n'), { isTrusted: true, supportThemeIcons: true }), - }); - - // Setup: Installing - Registry.as(ChatViewsWelcomeExtensions.ChatViewsWelcomeRegistry).register({ - title: defaultChat.chatWelcomeTitle, - when: ChatContextKeys.Setup.installing, - icon: defaultChat.icon, - disableFirstLinkToButton: true, - content: new MarkdownString([ - header, - localize('setupChatInstalling', "$(loading~spin) Setting up Chat for you..."), - footer, - `[${localize('learnMore', "Learn More")}](${defaultChat.documentationUrl})`, - ].join('\n\n'), { isTrusted: true, supportThemeIcons: true }), + content: () => ChatSetupWelcomeContent.getInstance(this.instantiationService, this.entitlementsResolver).element, }); } - private registerEntitlementListeners(): void { + private async checkExtensionInstallation(): Promise { this._register(this.extensionService.onDidChangeExtensions(result => { for (const extension of result.removed) { if (ExtensionIdentifier.equals(defaultChat.extensionId, extension.identifier)) { @@ -213,56 +126,134 @@ class ChatSetupContribution extends Disposable implements IWorkbenchContribution } })); + const extensions = await this.extensionManagementService.getInstalled(); + + const chatInstalled = !!extensions.find(value => ExtensionIdentifier.equals(value.identifier.id, defaultChat.extensionId)); + this.chatSetupState.update({ chatInstalled }); + } +} + +//#endregion + +//#region Entitlements Resolver + +type ChatSetupEntitlementClassification = { + entitled: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Flag indicating if the user is chat setup entitled' }; + entitlement: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Flag indicating the chat entitlement state' }; + owner: 'bpasero'; + comment: 'Reporting chat setup entitlements'; +}; + +type ChatSetupEntitlementEvent = { + entitled: boolean; + entitlement: ChatEntitlement; +}; + +class ChatSetupEntitlementResolver extends Disposable { + + private _entitlement = ChatEntitlement.Unknown; + get entitlement() { return this._entitlement; } + + private readonly _onDidChangeEntitlement = this._register(new Emitter()); + readonly onDidChangeEntitlement = this._onDidChangeEntitlement.event; + + private readonly chatSetupEntitledContextKey = ChatContextKeys.Setup.entitled.bindTo(this.contextKeyService); + + private resolvedEntitlement: ChatEntitlement | undefined = undefined; + + constructor( + @IContextKeyService private readonly contextKeyService: IContextKeyService, + @ITelemetryService private readonly telemetryService: ITelemetryService, + @IAuthenticationService private readonly authenticationService: IAuthenticationService, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @ILogService private readonly logService: ILogService, + ) { + super(); + + this.registerEntitlementListeners(); + this.registerAuthListeners(); + + this.handleDeclaredAuthProviders(); + } + + private registerEntitlementListeners(): void { this._register(this.authenticationService.onDidChangeSessions(e => { if (e.providerId === defaultChat.providerId) { if (e.event.added?.length) { - this.resolveEntitlement(e.event.added[0]); + this.resolveEntitlement(e.event.added.at(0)); } else if (e.event.removed?.length) { - this.chatSetupEntitledContextKey.set(false); + this.resolvedEntitlement = undefined; + this.update(this.toEntitlement(false)); } } })); this._register(this.authenticationService.onDidRegisterAuthenticationProvider(async e => { if (e.id === defaultChat.providerId) { - this.resolveEntitlement((await this.authenticationService.getSessions(e.id))[0]); + this.resolveEntitlement((await this.authenticationService.getSessions(e.id)).at(0)); } })); } private registerAuthListeners(): void { - const hasProviderSessions = async () => { - const sessions = await this.authenticationService.getSessions(defaultChat.providerId); - return sessions.length > 0; - }; - - const handleDeclaredAuthProviders = async () => { - if (this.authenticationService.declaredProviders.find(p => p.id === defaultChat.providerId)) { - this.chatSetupSignedInContextKey.set(await hasProviderSessions()); - } - }; - this._register(this.authenticationService.onDidChangeDeclaredProviders(handleDeclaredAuthProviders)); - this._register(this.authenticationService.onDidRegisterAuthenticationProvider(handleDeclaredAuthProviders)); - - handleDeclaredAuthProviders(); + this._register(this.authenticationService.onDidChangeDeclaredProviders(() => this.handleDeclaredAuthProviders())); + this._register(this.authenticationService.onDidRegisterAuthenticationProvider(() => this.handleDeclaredAuthProviders())); this._register(this.authenticationService.onDidChangeSessions(async ({ providerId }) => { if (providerId === defaultChat.providerId) { - this.chatSetupSignedInContextKey.set(await hasProviderSessions()); + this.update(this.toEntitlement(await this.hasProviderSessions())); } })); } + private toEntitlement(hasSession: boolean, skuLimitedAvailable?: boolean): ChatEntitlement { + if (!hasSession) { + return ChatEntitlement.Unknown; + } + + if (typeof this.resolvedEntitlement !== 'undefined') { + return this.resolvedEntitlement; + } + + if (typeof skuLimitedAvailable === 'boolean') { + return skuLimitedAvailable ? ChatEntitlement.Available : ChatEntitlement.Unavailable; + } + + return ChatEntitlement.Unresolved; + } + + private async handleDeclaredAuthProviders(): Promise { + if (this.authenticationService.declaredProviders.find(provider => provider.id === defaultChat.providerId)) { + this.update(this.toEntitlement(await this.hasProviderSessions())); + } + } + + private async hasProviderSessions(): Promise { + const sessions = await this.authenticationService.getSessions(defaultChat.providerId); + for (const session of sessions) { + for (const scopes of defaultChat.providerScopes) { + if (this.scopesMatch(session.scopes, scopes)) { + return true; + } + } + } + + return false; + } + + private scopesMatch(scopes: ReadonlyArray, expectedScopes: string[]): boolean { + return scopes.length === expectedScopes.length && expectedScopes.every(scope => scopes.includes(scope)); + } + private async resolveEntitlement(session: AuthenticationSession | undefined): Promise { if (!session) { return; } - const entitlement = await this.doResolveEntitlement(session); - this.chatSetupEntitledContextKey.set(!!entitlement.chatEnabled); + this.update(await this.doResolveEntitlement(session)); } - private async doResolveEntitlement(session: AuthenticationSession): Promise { + private async doResolveEntitlement(session: AuthenticationSession): Promise { if (this.resolvedEntitlement) { return this.resolvedEntitlement; } @@ -270,58 +261,282 @@ class ChatSetupContribution extends Disposable implements IWorkbenchContribution const cts = new CancellationTokenSource(); this._register(toDisposable(() => cts.dispose(true))); - const context = await this.instantiationService.invokeFunction(accessor => ChatSetupRequestHelper.request(accessor, defaultChat.entitlementUrl, 'GET', session, cts.token)); - if (!context) { - return UNKNOWN_CHAT_ENTITLEMENT; + const response = await this.instantiationService.invokeFunction(accessor => ChatSetupRequestHelper.request(accessor, defaultChat.entitlementUrl, 'GET', undefined, session, cts.token)); + if (!response) { + this.logService.trace('[chat setup] entitlement: no response'); + return ChatEntitlement.Unresolved; } - if (context.res.statusCode && context.res.statusCode !== 200) { - return UNKNOWN_CHAT_ENTITLEMENT; + if (response.res.statusCode && response.res.statusCode !== 200) { + this.logService.trace(`[chat setup] entitlement: unexpected status code ${response.res.statusCode}`); + return ChatEntitlement.Unresolved; } - const result = await asText(context); + const result = await asText(response); if (!result) { - return UNKNOWN_CHAT_ENTITLEMENT; + this.logService.trace('[chat setup] entitlement: response has no content'); + return ChatEntitlement.Unresolved; } let parsedResult: any; try { parsedResult = JSON.parse(result); + this.logService.trace(`[chat setup] entitlement: parsed result is ${JSON.stringify(parsedResult)}`); } catch (err) { - return UNKNOWN_CHAT_ENTITLEMENT; + this.logService.trace(`[chat setup] entitlement: error parsing response (${err})`); + return ChatEntitlement.Unresolved; } - this.resolvedEntitlement = { - chatEnabled: Boolean(parsedResult[defaultChat.entitlementChatEnabled]), - chatSku30DTrial: parsedResult[defaultChat.entitlementSkuKey] === defaultChat.entitlementSku30DTrialValue - }; + const entitled = Boolean(parsedResult[defaultChat.entitlementChatEnabled]); + this.chatSetupEntitledContextKey.set(entitled); + + const skuLimitedAvailable = Boolean(parsedResult[defaultChat.entitlementSkuLimitedEnabled]); + this.resolvedEntitlement = this.toEntitlement(entitled, skuLimitedAvailable); + + this.logService.trace(`[chat setup] entitlement: resolved to ${this.resolvedEntitlement}`); - this.telemetryService.publicLog2('chatInstallEntitlement', { - entitled: !!this.resolvedEntitlement.chatEnabled, - trial: !!this.resolvedEntitlement.chatSku30DTrial + this.telemetryService.publicLog2('chatInstallEntitlement', { + entitled, + entitlement: this.resolvedEntitlement }); return this.resolvedEntitlement; } - private async checkExtensionInstallation(): Promise { - const extensions = await this.extensionManagementService.getInstalled(); + private update(newEntitlement: ChatEntitlement): void { + const entitlement = this._entitlement; + this._entitlement = newEntitlement; + if (entitlement !== this._entitlement) { + this._onDidChangeEntitlement.fire(this._entitlement); + } + } +} - const chatInstalled = !!extensions.find(value => ExtensionIdentifier.equals(value.identifier.id, defaultChat.extensionId)); - this.chatSetupState.update({ chatInstalled }); +//#endregion + +//#region Setup Rendering + +type InstallChatClassification = { + owner: 'bpasero'; + comment: 'Provides insight into chat installation.'; + installResult: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the extension was installed successfully, cancelled or failed to install.' }; + signedIn: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the user did sign in prior to installing the extension.' }; +}; +type InstallChatEvent = { + installResult: 'installed' | 'cancelled' | 'failedInstall' | 'failedNotSignedIn'; + signedIn: boolean; +}; + +interface IChatSetupWelcomeContentOptions { + readonly entitlement: ChatEntitlement; + readonly onDidChangeEntitlement: Event; +} + +class ChatSetupWelcomeContent extends Disposable { + + private static INSTANCE: ChatSetupWelcomeContent | undefined; + static getInstance(instantiationService: IInstantiationService, options: IChatSetupWelcomeContentOptions): ChatSetupWelcomeContent { + if (!ChatSetupWelcomeContent.INSTANCE) { + ChatSetupWelcomeContent.INSTANCE = instantiationService.createInstance(ChatSetupWelcomeContent, options); + } + + return ChatSetupWelcomeContent.INSTANCE; + } + + readonly element = $('.chat-setup-view'); + + constructor( + private readonly options: IChatSetupWelcomeContentOptions, + @ITelemetryService private readonly telemetryService: ITelemetryService, + @IAuthenticationService private readonly authenticationService: IAuthenticationService, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IViewsService private readonly viewsService: IViewsService, + @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, + @IProductService private readonly productService: IProductService, + @ILogService private readonly logService: ILogService, + ) { + super(); + + this.create(); + } + + private create(): void { + const markdown = this._register(this.instantiationService.createInstance(MarkdownRenderer, {})); + + // Header + this.element.appendChild($('p')).textContent = localize('setupHeader', "{0} is your AI pair programmer.", defaultChat.name); + + // SKU Limited Sign-up + const skuHeader = localize({ key: 'skuHeader', comment: ['{Locked="]({0})"}'] }, "Setup will sign you up to {0} [limited access]({1}).", defaultChat.name, defaultChat.skusDocumentationUrl); + const skuHeaderElement = this.element.appendChild($('p')).appendChild(this._register(markdown.render(new MarkdownString(skuHeader, { isTrusted: true }))).element); + + const telemetryLabel = localize('telemetryLabel', "Allow {0} to use my data, including Prompts, Suggestions, and Code Snippets, for product improvements", defaultChat.providerName); + const { container: telemetryContainer, checkbox: telemetryCheckbox } = this.createCheckBox(telemetryLabel, this.telemetryService.telemetryLevel === TelemetryLevel.NONE ? false : false); + + const detectionLabel = localize('detectionLabel', "Allow suggestions matching public code"); + const { container: detectionContainer, checkbox: detectionCheckbox } = this.createCheckBox(detectionLabel, true); + + // Setup Button + let setupRunning = false; + + const buttonRow = this.element.appendChild($('p')); + + const button = this._register(new Button(buttonRow, { ...defaultButtonStyles, supportIcons: true })); + this.updateControls(button, [telemetryCheckbox, detectionCheckbox], false); + + this._register(button.onDidClick(async () => { + setupRunning = true; + this.updateControls(button, [telemetryCheckbox, detectionCheckbox], setupRunning); + + try { + await this.setup(telemetryCheckbox?.checked, detectionCheckbox?.checked); + } finally { + setupRunning = false; + } + + this.updateControls(button, [telemetryCheckbox, detectionCheckbox], setupRunning); + })); + + // Footer + const footer = localize({ key: 'privacyFooter', comment: ['{Locked="]({0})"}'] }, "By proceeding you agree to our [privacy statement]({0}). You can [learn more]({1}) about {2}.", defaultChat.privacyStatementUrl, defaultChat.documentationUrl, defaultChat.name); + this.element.appendChild($('p')).appendChild(this._register(markdown.render(new MarkdownString(footer, { isTrusted: true }))).element); + + // Update based on entilement changes + this._register(this.options.onDidChangeEntitlement(() => { + if (setupRunning) { + return; // do not change when setup running + } + setVisibility(this.options.entitlement !== ChatEntitlement.Unavailable, skuHeaderElement, telemetryContainer, detectionContainer); + this.updateControls(button, [telemetryCheckbox, detectionCheckbox], setupRunning); + })); + } + + private createCheckBox(label: string, checked: boolean): { container: HTMLElement; checkbox: Checkbox } { + const container = this.element.appendChild($('p')); + const checkbox = this._register(new Checkbox(label, checked, defaultCheckboxStyles)); + container.appendChild(checkbox.domNode); + + const telemetryCheckboxLabelContainer = container.appendChild($('div')); + telemetryCheckboxLabelContainer.textContent = label; + this._register(addDisposableListener(telemetryCheckboxLabelContainer, EventType.CLICK, () => { + if (checkbox?.enabled) { + checkbox.checked = !checkbox.checked; + } + })); + + return { container, checkbox }; + } + + private updateControls(button: Button, checkboxes: Checkbox[], setupRunning: boolean): void { + if (setupRunning) { + button.enabled = false; + button.label = localize('setupChatInstalling', "$(loading~spin) Completing Setup..."); + + for (const checkbox of checkboxes) { + checkbox.disable(); + } + } else { + button.enabled = true; + button.label = this.options.entitlement === ChatEntitlement.Unknown ? + localize('signInAndSetup', "Sign in and Complete Setup") : + localize('setup', "Complete Setup"); + + for (const checkbox of checkboxes) { + checkbox.enable(); + } + } + } + + private async setup(enableTelemetry: boolean | undefined, enableDetection: boolean | undefined): Promise { + let session: AuthenticationSession | undefined; + if (this.options.entitlement === ChatEntitlement.Unknown) { + session = await this.signIn(); + if (!session) { + return false; // user cancelled + } + } + + return this.install(session, enableTelemetry, enableDetection); + } + + private async signIn(): Promise { + let session: AuthenticationSession | undefined; + try { + showChatView(this.viewsService); + session = await this.authenticationService.createSession(defaultChat.providerId, defaultChat.providerScopes[0]); + } catch (error) { + // noop + } + + if (!session) { + this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult: 'failedNotSignedIn', signedIn: false }); + } + + return session; + } + + private async install(session: AuthenticationSession | undefined, enableTelemetry: boolean | undefined, enableDetection: boolean | undefined): Promise { + const signedIn = !!session; + const activeElement = getActiveElement(); + + let installResult: 'installed' | 'cancelled' | 'failedInstall'; + try { + showChatView(this.viewsService); + + if (this.options.entitlement !== ChatEntitlement.Unavailable) { + const body = { + public_code_suggestions: enableDetection ? 'enabled' : 'disabled', + restricted_telemetry: enableTelemetry ? 'enabled' : 'disabled' + }; + this.logService.trace(`[chat setup] install: signing up to limited SKU with ${JSON.stringify(body)}`); + + const response = await this.instantiationService.invokeFunction(accessor => ChatSetupRequestHelper.request(accessor, defaultChat.entitlementSkuLimitedUrl, 'POST', body, session, CancellationToken.None)); + if (response && this.logService.getLevel() === LogLevel.Trace) { + this.logService.trace(`[chat setup] install: response from signing up to limited SKU ${JSON.stringify(await asText(response))}`); + } + } else { + this.logService.trace('[chat setup] install: not signing up to limited SKU'); + } + + await this.extensionsWorkbenchService.install(defaultChat.extensionId, { + enable: true, + isMachineScoped: false, + installPreReleaseVersion: this.productService.quality !== 'stable' + }, ChatViewId); + + installResult = 'installed'; + } catch (error) { + this.logService.trace(`[chat setup] install: error ${error}`); + + installResult = isCancellationError(error) ? 'cancelled' : 'failedInstall'; + } + + this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult, signedIn }); + + if (activeElement === getActiveElement()) { + (await showChatView(this.viewsService))?.focusInput(); + } + + return installResult === 'installed'; } } +//#endregion + +//#region Helpers + class ChatSetupRequestHelper { - static async request(accessor: ServicesAccessor, url: string, type: 'GET' | 'POST', session: AuthenticationSession | undefined, token: CancellationToken): Promise { + static async request(accessor: ServicesAccessor, url: string, type: 'GET', body: undefined, session: AuthenticationSession | undefined, token: CancellationToken): Promise; + static async request(accessor: ServicesAccessor, url: string, type: 'POST', body: object, session: AuthenticationSession | undefined, token: CancellationToken): Promise; + static async request(accessor: ServicesAccessor, url: string, type: 'GET' | 'POST', body: object | undefined, session: AuthenticationSession | undefined, token: CancellationToken): Promise { const requestService = accessor.get(IRequestService); const logService = accessor.get(ILogService); const authenticationService = accessor.get(IAuthenticationService); try { if (!session) { - session = (await authenticationService.getSessions(defaultChat.providerId))[0]; + session = (await authenticationService.getSessions(defaultChat.providerId)).at(0); } if (!session) { @@ -331,13 +546,13 @@ class ChatSetupRequestHelper { return await requestService.request({ type, url, - data: type === 'POST' ? JSON.stringify({}) : undefined, + data: type === 'POST' ? JSON.stringify(body) : undefined, headers: { 'Authorization': `Bearer ${session.accessToken}` } }, token); } catch (error) { - logService.error(error); + logService.error(`[chat setup] request: error ${error}`); return undefined; } @@ -355,7 +570,7 @@ class ChatSetupState { constructor( @IContextKeyService private readonly contextKeyService: IContextKeyService, @IStorageService private readonly storageService: IStorageService, - @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService + @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, ) { this.updateContext(); } @@ -395,15 +610,27 @@ class ChatSetupState { } } +//#endregion + +//#region Actions + class ChatSetupTriggerAction extends Action2 { static readonly ID = 'workbench.action.chat.triggerSetup'; - static readonly TITLE = localize2('triggerChatSetup', "Trigger Chat Setup"); + static readonly TITLE = localize2('triggerChatSetup', "Setup {0}...", defaultChat.name); constructor() { super({ id: ChatSetupTriggerAction.ID, - title: ChatSetupTriggerAction.TITLE + title: ChatSetupTriggerAction.TITLE, + f1: true, + precondition: ChatContextKeys.Setup.installed.negate(), + menu: { + id: MenuId.ChatCommandCenter, + group: 'a_first', + order: 1, + when: ChatContextKeys.Setup.installed.negate() + } }); } @@ -427,14 +654,11 @@ class ChatSetupHideAction extends Action2 { id: ChatSetupHideAction.ID, title: ChatSetupHideAction.TITLE, f1: true, - precondition: ContextKeyExpr.and( - ChatContextKeys.Setup.triggered, - ChatContextKeys.Setup.installed.negate() - ), + precondition: ChatContextKeys.Setup.installed.negate(), menu: { id: MenuId.ChatCommandCenter, group: 'a_first', - order: 1, + order: 2, when: ChatContextKeys.Setup.installed.negate() } }); @@ -472,128 +696,9 @@ class ChatSetupHideAction extends Action2 { } } -class ChatSetupInstallAction extends Action2 { - - static readonly ID = 'workbench.action.chat.install'; - static readonly TITLE = localize2('installChat', "Install {0}", defaultChat.name); - - constructor() { - super({ - id: ChatSetupInstallAction.ID, - title: ChatSetupInstallAction.TITLE, - category: CHAT_CATEGORY, - menu: { - id: MenuId.ChatCommandCenter, - group: 'a_first', - order: 0, - when: ContextKeyExpr.and( - ChatContextKeys.Setup.signedIn, - ChatContextKeys.Setup.installed.negate() - ) - } - }); - } - - override run(accessor: ServicesAccessor): Promise { - return ChatSetupInstallAction.install(accessor, undefined); - } - - static async install(accessor: ServicesAccessor, session: AuthenticationSession | undefined) { - const extensionsWorkbenchService = accessor.get(IExtensionsWorkbenchService); - const productService = accessor.get(IProductService); - const telemetryService = accessor.get(ITelemetryService); - const contextKeyService = accessor.get(IContextKeyService); - const viewsService = accessor.get(IViewsService); - const chatAgentService = accessor.get(IChatAgentService); - const instantiationService = accessor.get(IInstantiationService); - - const signedIn = !!session; - const setupInstallingContextKey = ChatContextKeys.Setup.installing.bindTo(contextKeyService); - const activeElement = getActiveElement(); - - let installResult: 'installed' | 'cancelled' | 'failedInstall'; - try { - setupInstallingContextKey.set(true); - showChatView(viewsService); - - await instantiationService.invokeFunction(accessor => ChatSetupRequestHelper.request(accessor, defaultChat.entitlementSkuAlternateUrl, 'POST', session, CancellationToken.None)); - - await extensionsWorkbenchService.install(defaultChat.extensionId, { - enable: true, - isMachineScoped: false, - installPreReleaseVersion: productService.quality !== 'stable' - }, ChatViewId); - - installResult = 'installed'; - } catch (error) { - installResult = isCancellationError(error) ? 'cancelled' : 'failedInstall'; - } - - telemetryService.publicLog2('commandCenter.chatInstall', { installResult, signedIn }); - - await Promise.race([timeout(5000), Event.toPromise(chatAgentService.onDidChangeAgents)]); // reduce flicker (https://github.com/microsoft/vscode-copilot/issues/9274) - - setupInstallingContextKey.reset(); - - if (activeElement === getActiveElement()) { - (await showChatView(viewsService))?.focusInput(); - } - } -} - -class ChatSetupSignInAndInstallChatAction extends Action2 { - - static readonly ID = 'workbench.action.chat.signInAndInstall'; - static readonly TITLE = localize2('signInAndInstallChat', "Sign in to use {0}", defaultChat.name); - - constructor() { - super({ - id: ChatSetupSignInAndInstallChatAction.ID, - title: ChatSetupSignInAndInstallChatAction.TITLE, - category: CHAT_CATEGORY, - menu: { - id: MenuId.ChatCommandCenter, - group: 'a_first', - order: 0, - when: ContextKeyExpr.and( - ChatContextKeys.Setup.signedIn.negate(), - ChatContextKeys.Setup.installed.negate() - ) - } - }); - } - - override async run(accessor: ServicesAccessor): Promise { - const authenticationService = accessor.get(IAuthenticationService); - const instantiationService = accessor.get(IInstantiationService); - const telemetryService = accessor.get(ITelemetryService); - const contextKeyService = accessor.get(IContextKeyService); - const viewsService = accessor.get(IViewsService); - - const setupSigningInContextKey = ChatContextKeys.Setup.signingIn.bindTo(contextKeyService); - - let session: AuthenticationSession | undefined; - try { - setupSigningInContextKey.set(true); - showChatView(viewsService); - session = await authenticationService.createSession(defaultChat.providerId, defaultChat.providerScopes); - } catch (error) { - // noop - } finally { - setupSigningInContextKey.reset(); - } - - if (session) { - instantiationService.invokeFunction(accessor => ChatSetupInstallAction.install(accessor, session)); - } else { - telemetryService.publicLog2('commandCenter.chatInstall', { installResult: 'failedNotSignedIn', signedIn: false }); - } - } -} +//#endregion registerAction2(ChatSetupTriggerAction); registerAction2(ChatSetupHideAction); -registerAction2(ChatSetupInstallAction); -registerAction2(ChatSetupSignInAndInstallChatAction); registerWorkbenchContribution2('workbench.chat.setup', ChatSetupContribution, WorkbenchPhase.BlockRestore); diff --git a/src/vs/workbench/contrib/chat/browser/media/chatViewSetup.css b/src/vs/workbench/contrib/chat/browser/media/chatViewSetup.css new file mode 100644 index 0000000000000..988c082bdc611 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/media/chatViewSetup.css @@ -0,0 +1,25 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.chat-welcome-view .chat-setup-view { + text-align: initial; + + p { + width: 100%; + } + + .codicon[class*='codicon-'] { + font-size: 13px; + line-height: 1.4em; + vertical-align: bottom; + } + + .monaco-button { + text-align: center; + display: inline-block; + width: 100%; + padding: 4px 7px; + } +} diff --git a/src/vs/workbench/contrib/chat/browser/media/chatViewWelcome.css b/src/vs/workbench/contrib/chat/browser/media/chatViewWelcome.css index dffc746a0072c..657d1f5869a78 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chatViewWelcome.css +++ b/src/vs/workbench/contrib/chat/browser/media/chatViewWelcome.css @@ -68,12 +68,6 @@ div.chat-welcome-view { a { color: var(--vscode-textLink-foreground); } - - .codicon[class*='codicon-'] { - font-size: 13px; - line-height: 1.4em; - vertical-align: bottom; - } } .monaco-button { diff --git a/src/vs/workbench/contrib/chat/browser/viewsWelcome/chatViewWelcomeController.ts b/src/vs/workbench/contrib/chat/browser/viewsWelcome/chatViewWelcomeController.ts index c68a5442d8cca..b9f7624d6cf00 100644 --- a/src/vs/workbench/contrib/chat/browser/viewsWelcome/chatViewWelcomeController.ts +++ b/src/vs/workbench/contrib/chat/browser/viewsWelcome/chatViewWelcomeController.ts @@ -82,13 +82,20 @@ export class ChatViewWelcomeController extends Disposable { this.renderDisposables.clear(); dom.clearNode(this.element!); - const enabledDescriptor = descriptors.find(d => this.contextKeyService.contextMatchesRules(d.when)); + const matchingDescriptors = descriptors.filter(descriptor => this.contextKeyService.contextMatchesRules(descriptor.when)); + let enabledDescriptor: IChatViewsWelcomeDescriptor | undefined; + for (const descriptor of matchingDescriptors) { + if (typeof descriptor.content === 'function') { + enabledDescriptor = descriptor; // when multiple descriptors match, prefer a "core" one over a "descriptive" one + break; + } + } + enabledDescriptor = enabledDescriptor ?? matchingDescriptors.at(0); if (enabledDescriptor) { const content: IChatViewWelcomeContent = { icon: enabledDescriptor.icon, title: enabledDescriptor.title, - message: enabledDescriptor.content, - disableFirstLinkToButton: enabledDescriptor.disableFirstLinkToButton, + message: enabledDescriptor.content }; const welcomeView = this.renderDisposables.add(this.instantiationService.createInstance(ChatViewWelcomePart, content, { firstLinkToButton: true, location: this.location })); this.element!.appendChild(welcomeView.element); @@ -102,8 +109,7 @@ export class ChatViewWelcomeController extends Disposable { export interface IChatViewWelcomeContent { icon?: ThemeIcon; title: string; - message: IMarkdownString; - disableFirstLinkToButton?: boolean; + message: IMarkdownString | ((disposables: DisposableStore) => HTMLElement); tips?: IMarkdownString; } @@ -126,38 +132,47 @@ export class ChatViewWelcomePart extends Disposable { this.element = dom.$('.chat-welcome-view'); try { + const renderer = this._register(this.instantiationService.createInstance(MarkdownRenderer, {})); + + // Icon const icon = dom.append(this.element, $('.chat-welcome-view-icon')); + if (content.icon) { + icon.appendChild(renderIcon(content.icon)); + } + + // Title const title = dom.append(this.element, $('.chat-welcome-view-title')); + title.textContent = content.title; + // Preview indicator if (options?.location === ChatAgentLocation.EditingSession) { const featureIndicator = dom.append(this.element, $('.chat-welcome-view-indicator')); featureIndicator.textContent = localize('preview', 'PREVIEW'); } + // Message const message = dom.append(this.element, $('.chat-welcome-view-message')); - - if (content.icon) { - icon.appendChild(renderIcon(content.icon)); - } - - title.textContent = content.title; - const renderer = this.instantiationService.createInstance(MarkdownRenderer, {}); - const messageResult = this._register(renderer.render(content.message)); - const firstLink = options?.firstLinkToButton && !content.disableFirstLinkToButton ? messageResult.element.querySelector('a') : undefined; - if (firstLink) { - const target = firstLink.getAttribute('data-href'); - const button = this._register(new Button(firstLink.parentElement!, defaultButtonStyles)); - button.label = firstLink.textContent ?? ''; - if (target) { - this._register(button.onDidClick(() => { - this.openerService.open(target, { allowCommands: true }); - })); + if (typeof content.message === 'function') { + dom.append(message, content.message(this._register(new DisposableStore()))); + } else { + const messageResult = this._register(renderer.render(content.message)); + const firstLink = options?.firstLinkToButton ? messageResult.element.querySelector('a') : undefined; + if (firstLink) { + const target = firstLink.getAttribute('data-href'); + const button = this._register(new Button(firstLink.parentElement!, defaultButtonStyles)); + button.label = firstLink.textContent ?? ''; + if (target) { + this._register(button.onDidClick(() => { + this.openerService.open(target, { allowCommands: true }); + })); + } + firstLink.replaceWith(button.element); } - firstLink.replaceWith(button.element); - } - dom.append(message, messageResult.element); + dom.append(message, messageResult.element); + } + // Tips if (content.tips) { const tips = dom.append(this.element, $('.chat-welcome-view-tips')); const tipsResult = this._register(renderer.render(content.tips)); diff --git a/src/vs/workbench/contrib/chat/browser/viewsWelcome/chatViewsWelcome.ts b/src/vs/workbench/contrib/chat/browser/viewsWelcome/chatViewsWelcome.ts index b08baae1460bc..e0e7383bf403e 100644 --- a/src/vs/workbench/contrib/chat/browser/viewsWelcome/chatViewsWelcome.ts +++ b/src/vs/workbench/contrib/chat/browser/viewsWelcome/chatViewsWelcome.ts @@ -5,6 +5,7 @@ import { Emitter, Event } from '../../../../../base/common/event.js'; import { IMarkdownString } from '../../../../../base/common/htmlContent.js'; +import { DisposableStore } from '../../../../../base/common/lifecycle.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; import { ContextKeyExpression } from '../../../../../platform/contextkey/common/contextkey.js'; import { Registry } from '../../../../../platform/registry/common/platform.js'; @@ -16,8 +17,7 @@ export const enum ChatViewsWelcomeExtensions { export interface IChatViewsWelcomeDescriptor { icon?: ThemeIcon; title: string; - content: IMarkdownString; - disableFirstLinkToButton?: boolean; + content: IMarkdownString | ((disposables: DisposableStore) => HTMLElement); when: ContextKeyExpression; } diff --git a/src/vs/workbench/contrib/chat/common/chatContextKeys.ts b/src/vs/workbench/contrib/chat/common/chatContextKeys.ts index d5a31e868e169..9e12fc95c2202 100644 --- a/src/vs/workbench/contrib/chat/common/chatContextKeys.ts +++ b/src/vs/workbench/contrib/chat/common/chatContextKeys.ts @@ -42,13 +42,8 @@ export namespace ChatContextKeys { export const languageModelsAreUserSelectable = new RawContextKey('chatModelsAreUserSelectable', false, { type: 'boolean', description: localize('chatModelsAreUserSelectable', "True when the chat model can be selected manually by the user.") }); export const Setup = { - signedIn: new RawContextKey('chatSetupSignedIn', false, { type: 'boolean', description: localize('chatSetupSignedIn', "True when chat setup is offered for a signed-in user.") }), entitled: new RawContextKey('chatSetupEntitled', false, { type: 'boolean', description: localize('chatSetupEntitled', "True when chat setup is offered for a signed-in, entitled user.") }), - triggered: new RawContextKey('chatSetupTriggered', false, { type: 'boolean', description: localize('chatSetupTriggered', "True when chat setup is triggered.") }), - installing: new RawContextKey('chatSetupInstalling', false, { type: 'boolean', description: localize('chatSetupInstalling', "True when chat setup is installing chat.") }), - signingIn: new RawContextKey('chatSetupSigningIn', false, { type: 'boolean', description: localize('chatSetupSigningIn', "True when chat setup is waiting for signing in.") }), - installed: new RawContextKey('chatSetupInstalled', false, { type: 'boolean', description: localize('chatSetupInstalled', "True when the chat extension is installed.") }), }; } From 481f7099d4edd865d5879122dde8db6361cda4b5 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Sat, 23 Nov 2024 11:05:26 +0100 Subject: [PATCH 083/119] Git Blame - fix editor decoration hover (#234469) --- extensions/git/src/blame.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/git/src/blame.ts b/extensions/git/src/blame.ts index 207ca7b382c0a..b21b0b1ab372f 100644 --- a/extensions/git/src/blame.ts +++ b/extensions/git/src/blame.ts @@ -271,7 +271,6 @@ class GitBlameEditorDecoration { constructor(private readonly _controller: GitBlameController) { this._decorationType = window.createTextEditorDecorationType({ - isWholeLine: true, after: { color: new ThemeColor('git.blame.editorDecorationForeground') } @@ -333,7 +332,8 @@ class GitBlameEditorDecoration { range: new Range(position, position), renderOptions: { after: { - contentText: `\u00A0\u00A0\u00A0\u00A0\u00A0\u00A0\u00A0\u00A0\u00A0\u00A0${contentText}` + contentText: `${contentText}`, + margin: '0 0 0 50px' } }, }; From 86130d98a614705ab06c3d4e351dec2023b8de5e Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Sun, 24 Nov 2024 01:51:17 +0100 Subject: [PATCH 084/119] Git - add onDidCheckout extension API event (#234483) --- extensions/git/src/api/api1.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/extensions/git/src/api/api1.ts b/extensions/git/src/api/api1.ts index 63139af2447d6..62e3182b51550 100644 --- a/extensions/git/src/api/api1.ts +++ b/extensions/git/src/api/api1.ts @@ -12,7 +12,7 @@ import { toGitUri } from '../uri'; import { GitExtensionImpl } from './extension'; import { GitBaseApi } from '../git-base'; import { PickRemoteSourceOptions } from './git-base'; -import { Operation, OperationResult } from '../operation'; +import { OperationKind, OperationResult } from '../operation'; class ApiInputBox implements InputBox { set value(value: string) { this._inputBox.value = value; } @@ -67,7 +67,11 @@ export class ApiRepository implements Repository { readonly state: RepositoryState = new ApiRepositoryState(this.repository); readonly ui: RepositoryUIState = new ApiRepositoryUIState(this.repository.sourceControl); - readonly onDidCommit: Event = mapEvent(filterEvent(this.repository.onDidRunOperation, e => e.operation === Operation.Commit), () => null); + readonly onDidCommit: Event = mapEvent( + filterEvent(this.repository.onDidRunOperation, e => e.operation.kind === OperationKind.Commit), () => null); + + readonly onDidCheckout: Event = mapEvent( + filterEvent(this.repository.onDidRunOperation, e => e.operation.kind === OperationKind.Checkout || e.operation.kind === OperationKind.CheckoutTracking), () => null); constructor(readonly repository: BaseRepository) { } From f72a0a79d9048c49c30c04f9ab85c6f640b499db Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Sun, 24 Nov 2024 10:38:19 -0500 Subject: [PATCH 085/119] add tests for terminal suggest widget, fix some bugs (#234445) --- .vscode-test.js | 5 ++ build/gulpfile.extensions.js | 1 + extensions/terminal-suggest/package.json | 4 +- .../src/terminalSuggestMain.test.ts | 69 +++++++++++++++++++ .../src/terminalSuggestMain.ts | 41 +++++++---- extensions/terminal-suggest/tsconfig.json | 1 + scripts/test-integration.sh | 6 ++ 7 files changed, 111 insertions(+), 16 deletions(-) create mode 100644 extensions/terminal-suggest/src/terminalSuggestMain.test.ts diff --git a/.vscode-test.js b/.vscode-test.js index ce539a6572157..917413ede2acc 100644 --- a/.vscode-test.js +++ b/.vscode-test.js @@ -42,6 +42,11 @@ const extensions = [ workspaceFolder: `extensions/vscode-colorize-tests/test`, mocha: { timeout: 60_000 } }, + { + label: 'terminal-suggest', + workspaceFolder: path.join(os.tmpdir(), `terminal-suggest-${Math.floor(Math.random() * 100000)}`), + mocha: { timeout: 60_000 } + }, { label: 'vscode-colorize-perf-tests', workspaceFolder: `extensions/vscode-colorize-perf-tests/test`, diff --git a/build/gulpfile.extensions.js b/build/gulpfile.extensions.js index 13f27d6db4774..f05738faa620a 100644 --- a/build/gulpfile.extensions.js +++ b/build/gulpfile.extensions.js @@ -52,6 +52,7 @@ const compilations = [ 'extensions/markdown-math/tsconfig.json', 'extensions/media-preview/tsconfig.json', 'extensions/merge-conflict/tsconfig.json', + 'extensions/terminal-suggest/tsconfig.json', 'extensions/microsoft-authentication/tsconfig.json', 'extensions/notebook-renderers/tsconfig.json', 'extensions/npm/tsconfig.json', diff --git a/extensions/terminal-suggest/package.json b/extensions/terminal-suggest/package.json index aa4ca1909f51d..611ef00ed4ceb 100644 --- a/extensions/terminal-suggest/package.json +++ b/extensions/terminal-suggest/package.json @@ -17,8 +17,8 @@ "terminalCompletionProvider" ], "scripts": { - "compile": "npx gulp compile-extension:npm", - "watch": "npx gulp watch-extension:npm" + "compile": "npx gulp compile-extension:terminal-suggest", + "watch": "npx gulp watch-extension:terminal-suggest" }, "main": "./out/terminalSuggestMain", diff --git a/extensions/terminal-suggest/src/terminalSuggestMain.test.ts b/extensions/terminal-suggest/src/terminalSuggestMain.test.ts new file mode 100644 index 0000000000000..dfc3cd58e2217 --- /dev/null +++ b/extensions/terminal-suggest/src/terminalSuggestMain.test.ts @@ -0,0 +1,69 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { deepStrictEqual, strictEqual } from 'assert'; +import 'mocha'; +import { availableSpecs, getCompletionItemsFromSpecs } from './terminalSuggestMain'; + +suite('Terminal Suggest', () => { + + const availableCommands = ['cd', 'code', 'code-insiders']; + const codeOptions = ['-', '--add', '--category', '--diff', '--disable-extension', '--disable-extensions', '--disable-gpu', '--enable-proposed-api', '--extensions-dir', '--goto', '--help', '--inspect-brk-extensions', '--inspect-extensions', '--install-extension', '--list-extensions', '--locale', '--log', '--max-memory', '--merge', '--new-window', '--pre-release', '--prof-startup', '--profile', '--reuse-window', '--show-versions', '--status', '--sync', '--telemetry', '--uninstall-extension', '--user-data-dir', '--verbose', '--version', '--wait', '-a', '-d', '-g', '-h', '-m', '-n', '-r', '-s', '-v', '-w']; + + suite('Cursor at the end of the command line', () => { + createTestCase('|', availableCommands, 'neither', availableSpecs); + createTestCase('c|', availableCommands, 'neither', availableSpecs); + createTestCase('ls && c|', availableCommands, 'neither', availableSpecs); + createTestCase('cd |', ['~', '-'], 'folders', availableSpecs); + createTestCase('code|', ['code-insiders'], 'neither', availableSpecs); + createTestCase('code-insiders|', [], 'neither', availableSpecs); + createTestCase('code |', codeOptions, 'neither', availableSpecs); + createTestCase('code --locale |', ['bg', 'de', 'en', 'es', 'fr', 'hu', 'it', 'ja', 'ko', 'pt-br', 'ru', 'tr', 'zh-CN', 'zh-TW'], 'neither', availableSpecs); + createTestCase('code --diff |', [], 'files', availableSpecs); + createTestCase('code -di|', codeOptions.filter(o => o.startsWith('di')), 'neither', availableSpecs); + createTestCase('code --diff ./file1 |', [], 'files', availableSpecs); + createTestCase('code --merge |', [], 'files', availableSpecs); + createTestCase('code --merge ./file1 ./file2 |', [], 'files', availableSpecs); + createTestCase('code --merge ./file1 ./file2 ./base |', [], 'files', availableSpecs); + createTestCase('code --goto |', [], 'files', availableSpecs); + createTestCase('code --user-data-dir |', [], 'folders', availableSpecs); + createTestCase('code --profile |', [], 'neither', availableSpecs); + createTestCase('code --install-extension |', [], 'neither', availableSpecs); + createTestCase('code --uninstall-extension |', [], 'neither', availableSpecs); + createTestCase('code --log |', ['critical', 'error', 'warn', 'info', 'debug', 'trace', 'off'], 'neither', availableSpecs); + createTestCase('code --sync |', ['on', 'off'], 'neither', availableSpecs); + createTestCase('code --extensions-dir |', [], 'folders', availableSpecs); + createTestCase('code --list-extensions |', codeOptions, 'neither', availableSpecs); + createTestCase('code --show-versions |', codeOptions, 'neither', availableSpecs); + createTestCase('code --category |', ['azure', 'data science', 'debuggers', 'extension packs', 'education', 'formatters', 'keymaps', 'language packs', 'linters', 'machine learning', 'notebooks', 'programming languages', 'scm providers', 'snippets', 'testing', 'themes', 'visualization', 'other'], 'neither', availableSpecs); + createTestCase('code --category a|', ['azure'], 'neither', availableSpecs); + createTestCase('code-insiders --list-extensions |', codeOptions, 'neither', availableSpecs); + createTestCase('code-insiders --show-versions |', codeOptions, 'neither', availableSpecs); + createTestCase('code-insiders --category |', ['azure', 'data science', 'debuggers', 'extension packs', 'education', 'formatters', 'keymaps', 'language packs', 'linters', 'machine learning', 'notebooks', 'programming languages', 'scm providers', 'snippets', 'testing', 'themes', 'visualization', 'other'], 'neither', availableSpecs); + createTestCase('code-insiders --category a|', ['azure'], 'neither', availableSpecs); + createTestCase('code-insiders --category azure |', [], 'neither', availableSpecs); + }); + suite('Cursor not at the end of the line', () => { + createTestCase('code | --locale', codeOptions, 'neither', availableSpecs); + createTestCase('code --locale | && ls', ['bg', 'de', 'en', 'es', 'fr', 'hu', 'it', 'ja', 'ko', 'pt-br', 'ru', 'tr', 'zh-CN', 'zh-TW'], 'neither', availableSpecs); + createTestCase('code-insiders | --locale', codeOptions, 'neither', availableSpecs); + createTestCase('code-insiders --locale | && ls', ['bg', 'de', 'en', 'es', 'fr', 'hu', 'it', 'ja', 'ko', 'pt-br', 'ru', 'tr', 'zh-CN', 'zh-TW'], 'neither', availableSpecs); + }); + + function createTestCase(commandLineWithCursor: string, expectedCompletionLabels: string[], resourcesRequested: 'files' | 'folders' | 'both' | 'neither', availableSpecs: Fig.Spec[]): void { + const commandLine = commandLineWithCursor.split('|')[0]; + const cursorPosition = commandLineWithCursor.indexOf('|'); + const prefix = commandLine.slice(0, cursorPosition).split(' ').pop() || ''; + const filesRequested = resourcesRequested === 'files' || resourcesRequested === 'both'; + const foldersRequested = resourcesRequested === 'folders' || resourcesRequested === 'both'; + test(commandLineWithCursor, function () { + const result = getCompletionItemsFromSpecs(availableSpecs, { commandLine, cursorPosition }, availableCommands, prefix); + deepStrictEqual(result.items.map(i => i.label).sort(), expectedCompletionLabels.sort()); + strictEqual(result.filesRequested, filesRequested); + strictEqual(result.foldersRequested, foldersRequested); + }); + } +}); + diff --git a/extensions/terminal-suggest/src/terminalSuggestMain.ts b/extensions/terminal-suggest/src/terminalSuggestMain.ts index 1ff88ed9eb6bf..a6e03d65dcd65 100644 --- a/extensions/terminal-suggest/src/terminalSuggestMain.ts +++ b/extensions/terminal-suggest/src/terminalSuggestMain.ts @@ -14,6 +14,8 @@ import cdSpec from './completions/cd'; let cachedAvailableCommands: Set | undefined; let cachedBuiltinCommands: Map | undefined; +export const availableSpecs = [codeCompletionSpec, codeInsidersCompletionSpec, cdSpec]; + function getBuiltinCommands(shell: string): string[] | undefined { try { const shellType = path.basename(shell); @@ -89,8 +91,7 @@ export async function activate(context: vscode.ExtensionContext) { const items: vscode.TerminalCompletionItem[] = []; const prefix = getPrefix(terminalContext.commandLine, terminalContext.cursorPosition); - const specs = [codeCompletionSpec, codeInsidersCompletionSpec, cdSpec]; - const specCompletions = await getCompletionItemsFromSpecs(specs, terminalContext, new Set(commands), prefix, token); + const specCompletions = await getCompletionItemsFromSpecs(availableSpecs, terminalContext, commands, prefix, token); items.push(...specCompletions.items); let filesRequested = specCompletions.filesRequested; @@ -98,7 +99,7 @@ export async function activate(context: vscode.ExtensionContext) { if (!specCompletions.specificSuggestionsProvided) { for (const command of commands) { - if (command.startsWith(prefix)) { + if (command.startsWith(prefix) && !items.find(item => item.label === command)) { items.push(createCompletionItem(terminalContext.cursorPosition, prefix, command)); } } @@ -214,7 +215,7 @@ export function asArray(x: T | T[]): T[] { return Array.isArray(x) ? x : [x]; } -function getCompletionItemsFromSpecs(specs: Fig.Spec[], terminalContext: { commandLine: string; cursorPosition: number }, availableCommands: Set, prefix: string, token: vscode.CancellationToken): { items: vscode.TerminalCompletionItem[]; filesRequested: boolean; foldersRequested: boolean; specificSuggestionsProvided: boolean } { +export function getCompletionItemsFromSpecs(specs: Fig.Spec[], terminalContext: { commandLine: string; cursorPosition: number }, availableCommands: string[], prefix: string, token?: vscode.CancellationToken): { items: vscode.TerminalCompletionItem[]; filesRequested: boolean; foldersRequested: boolean; specificSuggestionsProvided: boolean } { const items: vscode.TerminalCompletionItem[] = []; let filesRequested = false; let foldersRequested = false; @@ -224,7 +225,21 @@ function getCompletionItemsFromSpecs(specs: Fig.Spec[], terminalContext: { comma continue; } for (const specLabel of specLabels) { - if (!availableCommands.has(specLabel) || token.isCancellationRequested || !terminalContext.commandLine.startsWith(specLabel)) { + if (!availableCommands.includes(specLabel) || (token && token?.isCancellationRequested)) { + continue; + } + // + if ( + // If the prompt is empty + !terminalContext.commandLine + // or the prefix matches the command and the prefix is not equal to the command + || !!prefix && specLabel.startsWith(prefix) && specLabel !== prefix + ) { + // push it to the completion items + items.push(createCompletionItem(terminalContext.cursorPosition, prefix, specLabel)); + } + if (!terminalContext.commandLine.startsWith(specLabel)) { + // the spec label is not the first word in the command line, so do not provide options or args continue; } const precedingText = terminalContext.commandLine.slice(0, terminalContext.cursorPosition + 1); @@ -235,7 +250,7 @@ function getCompletionItemsFromSpecs(specs: Fig.Spec[], terminalContext: { comma continue; } for (const optionLabel of optionLabels) { - if (optionLabel.startsWith(prefix) || (prefix.length > specLabel.length && prefix.trim() === specLabel)) { + if (!items.find(i => i.label === optionLabel) && optionLabel.startsWith(prefix) || (prefix.length > specLabel.length && prefix.trim() === specLabel)) { items.push(createCompletionItem(terminalContext.cursorPosition, prefix, optionLabel, option.description, false, vscode.TerminalCompletionItemKind.Flag)); } const expectedText = `${specLabel} ${optionLabel} `; @@ -248,13 +263,8 @@ function getCompletionItemsFromSpecs(specs: Fig.Spec[], terminalContext: { comma if (!argsCompletions) { continue; } - if (argsCompletions.specificSuggestionsProvided) { - // prevents the list from containing a bunch of other stuff - return argsCompletions; - } - items.push(...argsCompletions.items); - filesRequested = filesRequested || argsCompletions.filesRequested; - foldersRequested = foldersRequested || argsCompletions.foldersRequested; + // return early so that we don't show the other completions + return argsCompletions; } } } @@ -307,7 +317,10 @@ function getCompletionItemsFromArgs(args: Fig.SingleOrArray | undefined } for (const suggestionLabel of suggestionLabels) { - if (suggestionLabel && suggestionLabel.startsWith(currentPrefix.trim())) { + if (items.find(i => i.label === suggestionLabel)) { + continue; + } + if (suggestionLabel && suggestionLabel.startsWith(currentPrefix.trim()) && suggestionLabel !== currentPrefix.trim()) { const hasSpaceBeforeCursor = terminalContext.commandLine[terminalContext.cursorPosition - 1] === ' '; // prefix will be '' if there is a space before the cursor const description = typeof suggestion !== 'string' ? suggestion.description : ''; diff --git a/extensions/terminal-suggest/tsconfig.json b/extensions/terminal-suggest/tsconfig.json index 0fc7ab5f53c92..151a29616bb23 100644 --- a/extensions/terminal-suggest/tsconfig.json +++ b/extensions/terminal-suggest/tsconfig.json @@ -14,6 +14,7 @@ }, "include": [ "src/**/*", + "src/completions/index.d.ts", "../../src/vscode-dts/vscode.d.ts", "../../src/vscode-dts/vscode.proposed.terminalCompletionProvider.d.ts" ] diff --git a/scripts/test-integration.sh b/scripts/test-integration.sh index 35b358ae168f3..8900648030899 100755 --- a/scripts/test-integration.sh +++ b/scripts/test-integration.sh @@ -70,6 +70,12 @@ echo npm run test-extension -- -l vscode-colorize-tests kill_app +echo +echo "### Terminal Suggest tests" +echo +npm run test-extension -- -l terminal-suggest --enable-proposed-api=vscode.vscode-api-tests +kill_app + echo echo "### TypeScript tests" echo From 7e5533af2e374ea38d07d2d1f9214c3962d90d30 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 25 Nov 2024 05:19:38 +0100 Subject: [PATCH 086/119] chat - tweaks to welcome (#234500) --- package.json | 4 +- src/vs/base/common/product.ts | 6 +- .../chat/browser/chatSetup.contribution.ts | 469 ++++++++++-------- .../contrib/chat/common/chatContextKeys.ts | 3 +- 4 files changed, 283 insertions(+), 199 deletions(-) diff --git a/package.json b/package.json index 60c69be0d2518..5fa4e37886f80 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.96.0", - "distro": "0f524a5cfa305bf4a8cc06ca7fd2d4363ec8c7c1", + "distro": "b923ae4b113ee415bd170b90e6d8500a76516360", "author": { "name": "Microsoft Corporation" }, @@ -240,4 +240,4 @@ "optionalDependencies": { "windows-foreground-love": "0.5.0" } -} \ No newline at end of file +} diff --git a/src/vs/base/common/product.ts b/src/vs/base/common/product.ts index dbc89e5ddcc90..c5ad8ec863bb3 100644 --- a/src/vs/base/common/product.ts +++ b/src/vs/base/common/product.ts @@ -319,6 +319,8 @@ export interface IDefaultChatAgent { readonly providerScopes: string[][]; readonly entitlementUrl: string; readonly entitlementChatEnabled: string; - readonly entitlementSkuLimitedUrl: string; - readonly entitlementSkuLimitedEnabled: string; + readonly entitlementSignupLimitedUrl: string; + readonly entitlementCanSignupLimited: string; + readonly entitlementSkuType: string; + readonly entitlementSkuTypeLimited: string; } diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts b/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts index 038ec3f393077..bcd01d2344b2a 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts @@ -5,7 +5,7 @@ import './media/chatViewSetup.css'; import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js'; -import { Disposable, toDisposable } from '../../../../base/common/lifecycle.js'; +import { Disposable } from '../../../../base/common/lifecycle.js'; import { ContextKeyExpr, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { ITelemetryService, TelemetryLevel } from '../../../../platform/telemetry/common/telemetry.js'; import { AuthenticationSession, IAuthenticationService } from '../../../services/authentication/common/authentication.js'; @@ -58,14 +58,16 @@ const defaultChat = { providerScopes: product.defaultChatAgent?.providerScopes ?? [[]], entitlementUrl: product.defaultChatAgent?.entitlementUrl ?? '', entitlementChatEnabled: product.defaultChatAgent?.entitlementChatEnabled ?? '', - entitlementSkuLimitedUrl: product.defaultChatAgent?.entitlementSkuLimitedUrl ?? '', - entitlementSkuLimitedEnabled: product.defaultChatAgent?.entitlementSkuLimitedEnabled ?? '' + entitlementSignupLimitedUrl: product.defaultChatAgent?.entitlementSignupLimitedUrl ?? '', + entitlementCanSignupLimited: product.defaultChatAgent?.entitlementCanSignupLimited ?? '', + entitlementSkuType: product.defaultChatAgent?.entitlementSkuType ?? '', + entitlementSkuTypeLimited: product.defaultChatAgent?.entitlementSkuTypeLimited ?? '' }; enum ChatEntitlement { /** Signed out */ Unknown = 1, - /** Not yet resolved */ + /** Signed in but not yet resolved if Sign-up possible */ Unresolved, /** Signed in and entitled to Sign-up */ Available, @@ -77,8 +79,8 @@ enum ChatEntitlement { class ChatSetupContribution extends Disposable implements IWorkbenchContribution { - private readonly chatSetupState = this.instantiationService.createInstance(ChatSetupState); - private readonly entitlementsResolver = this._register(this.instantiationService.createInstance(ChatSetupEntitlementResolver)); + private readonly chatSetupContextKeys = this.instantiationService.createInstance(ChatSetupContextKeys); + private readonly entitlementsResolver = this._register(this.instantiationService.createInstance(ChatSetupEntitlementResolver, this.chatSetupContextKeys)); constructor( @IProductService private readonly productService: IProductService, @@ -113,14 +115,14 @@ class ChatSetupContribution extends Disposable implements IWorkbenchContribution this._register(this.extensionService.onDidChangeExtensions(result => { for (const extension of result.removed) { if (ExtensionIdentifier.equals(defaultChat.extensionId, extension.identifier)) { - this.chatSetupState.update({ chatInstalled: false }); + this.chatSetupContextKeys.update({ chatInstalled: false }); break; } } for (const extension of result.added) { if (ExtensionIdentifier.equals(defaultChat.extensionId, extension.identifier)) { - this.chatSetupState.update({ chatInstalled: true }); + this.chatSetupContextKeys.update({ chatInstalled: true }); break; } } @@ -129,7 +131,7 @@ class ChatSetupContribution extends Disposable implements IWorkbenchContribution const extensions = await this.extensionManagementService.getInstalled(); const chatInstalled = !!extensions.find(value => ExtensionIdentifier.equals(value.identifier.id, defaultChat.extensionId)); - this.chatSetupState.update({ chatInstalled }); + this.chatSetupContextKeys.update({ chatInstalled }); } } @@ -138,15 +140,17 @@ class ChatSetupContribution extends Disposable implements IWorkbenchContribution //#region Entitlements Resolver type ChatSetupEntitlementClassification = { - entitled: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Flag indicating if the user is chat setup entitled' }; entitlement: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Flag indicating the chat entitlement state' }; + entitled: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Flag indicating if the user is chat setup entitled' }; + limited: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Flag indicating if the user is chat setup limited' }; owner: 'bpasero'; comment: 'Reporting chat setup entitlements'; }; type ChatSetupEntitlementEvent = { - entitled: boolean; entitlement: ChatEntitlement; + entitled: boolean; + limited: boolean; }; class ChatSetupEntitlementResolver extends Disposable { @@ -157,12 +161,11 @@ class ChatSetupEntitlementResolver extends Disposable { private readonly _onDidChangeEntitlement = this._register(new Emitter()); readonly onDidChangeEntitlement = this._onDidChangeEntitlement.event; - private readonly chatSetupEntitledContextKey = ChatContextKeys.Setup.entitled.bindTo(this.contextKeyService); - + private pendingResolveCts = new CancellationTokenSource(); private resolvedEntitlement: ChatEntitlement | undefined = undefined; constructor( - @IContextKeyService private readonly contextKeyService: IContextKeyService, + private readonly chatSetupContextKeys: ChatSetupContextKeys, @ITelemetryService private readonly telemetryService: ITelemetryService, @IAuthenticationService private readonly authenticationService: IAuthenticationService, @IInstantiationService private readonly instantiationService: IInstantiationService, @@ -170,98 +173,99 @@ class ChatSetupEntitlementResolver extends Disposable { ) { super(); - this.registerEntitlementListeners(); - this.registerAuthListeners(); + this.registerListeners(); - this.handleDeclaredAuthProviders(); + this.resolve(); } - private registerEntitlementListeners(): void { + private registerListeners(): void { + this._register(this.authenticationService.onDidChangeDeclaredProviders(() => this.resolve())); + this._register(this.authenticationService.onDidChangeSessions(e => { if (e.providerId === defaultChat.providerId) { - if (e.event.added?.length) { - this.resolveEntitlement(e.event.added.at(0)); - } else if (e.event.removed?.length) { - this.resolvedEntitlement = undefined; - this.update(this.toEntitlement(false)); - } + this.resolve(); } })); - this._register(this.authenticationService.onDidRegisterAuthenticationProvider(async e => { + this._register(this.authenticationService.onDidRegisterAuthenticationProvider(e => { if (e.id === defaultChat.providerId) { - this.resolveEntitlement((await this.authenticationService.getSessions(e.id)).at(0)); + this.resolve(); } })); - } - - private registerAuthListeners(): void { - this._register(this.authenticationService.onDidChangeDeclaredProviders(() => this.handleDeclaredAuthProviders())); - this._register(this.authenticationService.onDidRegisterAuthenticationProvider(() => this.handleDeclaredAuthProviders())); - this._register(this.authenticationService.onDidChangeSessions(async ({ providerId }) => { - if (providerId === defaultChat.providerId) { - this.update(this.toEntitlement(await this.hasProviderSessions())); + this._register(this.authenticationService.onDidUnregisterAuthenticationProvider(e => { + if (e.id === defaultChat.providerId) { + this.resolve(); } })); } - private toEntitlement(hasSession: boolean, skuLimitedAvailable?: boolean): ChatEntitlement { - if (!hasSession) { - return ChatEntitlement.Unknown; - } + private async resolve(): Promise { + this.pendingResolveCts.dispose(true); + const cts = this.pendingResolveCts = new CancellationTokenSource(); - if (typeof this.resolvedEntitlement !== 'undefined') { - return this.resolvedEntitlement; + const session = await this.findMatchingProviderSession(cts.token); + if (cts.token.isCancellationRequested) { + return; } - if (typeof skuLimitedAvailable === 'boolean') { - return skuLimitedAvailable ? ChatEntitlement.Available : ChatEntitlement.Unavailable; + // Immediately signal whether we have a session or not + if (session) { + this.update(this.resolvedEntitlement ?? ChatEntitlement.Unresolved); + } else { + this.resolvedEntitlement = undefined; // reset resolved entitlement when there is no session + this.update(ChatEntitlement.Unknown); } - return ChatEntitlement.Unresolved; - } - - private async handleDeclaredAuthProviders(): Promise { - if (this.authenticationService.declaredProviders.find(provider => provider.id === defaultChat.providerId)) { - this.update(this.toEntitlement(await this.hasProviderSessions())); + if (session) { + // Afterwards resolve entitlement with a network request + this.resolveEntitlement(session, cts.token); } } - private async hasProviderSessions(): Promise { + private async findMatchingProviderSession(token: CancellationToken): Promise { const sessions = await this.authenticationService.getSessions(defaultChat.providerId); + if (token.isCancellationRequested) { + return undefined; + } + for (const session of sessions) { for (const scopes of defaultChat.providerScopes) { if (this.scopesMatch(session.scopes, scopes)) { - return true; + return session; } } } - return false; + return undefined; } private scopesMatch(scopes: ReadonlyArray, expectedScopes: string[]): boolean { return scopes.length === expectedScopes.length && expectedScopes.every(scope => scopes.includes(scope)); } - private async resolveEntitlement(session: AuthenticationSession | undefined): Promise { - if (!session) { - return; + private async resolveEntitlement(session: AuthenticationSession, token: CancellationToken): Promise { + if (typeof this.resolvedEntitlement !== 'undefined') { + return; // only resolve once } - this.update(await this.doResolveEntitlement(session)); + const entitlement = await this.doResolveEntitlement(session, token); + if (typeof entitlement === 'number' && !token.isCancellationRequested) { + this.resolvedEntitlement = entitlement; + this.update(entitlement); + } } - private async doResolveEntitlement(session: AuthenticationSession): Promise { - if (this.resolvedEntitlement) { - return this.resolvedEntitlement; + private async doResolveEntitlement(session: AuthenticationSession, token: CancellationToken): Promise { + if (token.isCancellationRequested) { + return undefined; } - const cts = new CancellationTokenSource(); - this._register(toDisposable(() => cts.dispose(true))); + const response = await this.instantiationService.invokeFunction(accessor => ChatSetupRequestHelper.request(accessor, defaultChat.entitlementUrl, 'GET', undefined, session, token)); + if (token.isCancellationRequested) { + return undefined; + } - const response = await this.instantiationService.invokeFunction(accessor => ChatSetupRequestHelper.request(accessor, defaultChat.entitlementUrl, 'GET', undefined, session, cts.token)); if (!response) { this.logService.trace('[chat setup] entitlement: no response'); return ChatEntitlement.Unresolved; @@ -272,35 +276,37 @@ class ChatSetupEntitlementResolver extends Disposable { return ChatEntitlement.Unresolved; } - const result = await asText(response); - if (!result) { + const responseText = await asText(response); + if (token.isCancellationRequested) { + return undefined; + } + + if (!responseText) { this.logService.trace('[chat setup] entitlement: response has no content'); return ChatEntitlement.Unresolved; } let parsedResult: any; try { - parsedResult = JSON.parse(result); + parsedResult = JSON.parse(responseText); this.logService.trace(`[chat setup] entitlement: parsed result is ${JSON.stringify(parsedResult)}`); } catch (err) { this.logService.trace(`[chat setup] entitlement: error parsing response (${err})`); return ChatEntitlement.Unresolved; } - const entitled = Boolean(parsedResult[defaultChat.entitlementChatEnabled]); - this.chatSetupEntitledContextKey.set(entitled); + const result = { + entitlement: Boolean(parsedResult[defaultChat.entitlementCanSignupLimited]) ? ChatEntitlement.Available : ChatEntitlement.Unavailable, + entitled: Boolean(parsedResult[defaultChat.entitlementChatEnabled]), + limited: Boolean(parsedResult[defaultChat.entitlementSkuType] === defaultChat.entitlementSkuTypeLimited) + }; - const skuLimitedAvailable = Boolean(parsedResult[defaultChat.entitlementSkuLimitedEnabled]); - this.resolvedEntitlement = this.toEntitlement(entitled, skuLimitedAvailable); + this.chatSetupContextKeys.update({ entitled: result.entitled, limited: result.limited }); - this.logService.trace(`[chat setup] entitlement: resolved to ${this.resolvedEntitlement}`); - - this.telemetryService.publicLog2('chatInstallEntitlement', { - entitled, - entitlement: this.resolvedEntitlement - }); + this.logService.trace(`[chat setup] entitlement: resolved to ${result.entitlement}, entitled: ${result.entitled}, limited: ${result.limited}`); + this.telemetryService.publicLog2('chatInstallEntitlement', result); - return this.resolvedEntitlement; + return result.entitlement; } private update(newEntitlement: ChatEntitlement): void { @@ -310,6 +316,16 @@ class ChatSetupEntitlementResolver extends Disposable { this._onDidChangeEntitlement.fire(this._entitlement); } } + + async forceResolveEntitlement(session: AuthenticationSession): Promise { + return this.doResolveEntitlement(session, CancellationToken.None); + } + + override dispose(): void { + this.pendingResolveCts.dispose(true); + + super.dispose(); + } } //#endregion @@ -327,136 +343,83 @@ type InstallChatEvent = { signedIn: boolean; }; -interface IChatSetupWelcomeContentOptions { - readonly entitlement: ChatEntitlement; - readonly onDidChangeEntitlement: Event; +enum ChatSetupStep { + Initial = 1, + SigningIn, + Installing } -class ChatSetupWelcomeContent extends Disposable { +class ChatSetupController extends Disposable { - private static INSTANCE: ChatSetupWelcomeContent | undefined; - static getInstance(instantiationService: IInstantiationService, options: IChatSetupWelcomeContentOptions): ChatSetupWelcomeContent { - if (!ChatSetupWelcomeContent.INSTANCE) { - ChatSetupWelcomeContent.INSTANCE = instantiationService.createInstance(ChatSetupWelcomeContent, options); - } + private readonly _onDidChange = this._register(new Emitter()); + readonly onDidChange = this._onDidChange.event; - return ChatSetupWelcomeContent.INSTANCE; + private _step = ChatSetupStep.Initial; + get step(): ChatSetupStep { + return this._step; } - readonly element = $('.chat-setup-view'); + get entitlement(): ChatEntitlement { + return this.entitlementResolver.entitlement; + } + + get canSignUpLimited(): boolean { + return this.entitlement === ChatEntitlement.Available || // user can sign up for limited + this.entitlement === ChatEntitlement.Unresolved; // user unresolved, play safe and allow + } constructor( - private readonly options: IChatSetupWelcomeContentOptions, + private readonly entitlementResolver: ChatSetupEntitlementResolver, @ITelemetryService private readonly telemetryService: ITelemetryService, @IAuthenticationService private readonly authenticationService: IAuthenticationService, @IInstantiationService private readonly instantiationService: IInstantiationService, @IViewsService private readonly viewsService: IViewsService, @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, @IProductService private readonly productService: IProductService, - @ILogService private readonly logService: ILogService, + @ILogService private readonly logService: ILogService ) { super(); - this.create(); + this.registerListeners(); } - private create(): void { - const markdown = this._register(this.instantiationService.createInstance(MarkdownRenderer, {})); - - // Header - this.element.appendChild($('p')).textContent = localize('setupHeader', "{0} is your AI pair programmer.", defaultChat.name); - - // SKU Limited Sign-up - const skuHeader = localize({ key: 'skuHeader', comment: ['{Locked="]({0})"}'] }, "Setup will sign you up to {0} [limited access]({1}).", defaultChat.name, defaultChat.skusDocumentationUrl); - const skuHeaderElement = this.element.appendChild($('p')).appendChild(this._register(markdown.render(new MarkdownString(skuHeader, { isTrusted: true }))).element); - - const telemetryLabel = localize('telemetryLabel', "Allow {0} to use my data, including Prompts, Suggestions, and Code Snippets, for product improvements", defaultChat.providerName); - const { container: telemetryContainer, checkbox: telemetryCheckbox } = this.createCheckBox(telemetryLabel, this.telemetryService.telemetryLevel === TelemetryLevel.NONE ? false : false); - - const detectionLabel = localize('detectionLabel', "Allow suggestions matching public code"); - const { container: detectionContainer, checkbox: detectionCheckbox } = this.createCheckBox(detectionLabel, true); - - // Setup Button - let setupRunning = false; - - const buttonRow = this.element.appendChild($('p')); - - const button = this._register(new Button(buttonRow, { ...defaultButtonStyles, supportIcons: true })); - this.updateControls(button, [telemetryCheckbox, detectionCheckbox], false); - - this._register(button.onDidClick(async () => { - setupRunning = true; - this.updateControls(button, [telemetryCheckbox, detectionCheckbox], setupRunning); - - try { - await this.setup(telemetryCheckbox?.checked, detectionCheckbox?.checked); - } finally { - setupRunning = false; - } - - this.updateControls(button, [telemetryCheckbox, detectionCheckbox], setupRunning); - })); - - // Footer - const footer = localize({ key: 'privacyFooter', comment: ['{Locked="]({0})"}'] }, "By proceeding you agree to our [privacy statement]({0}). You can [learn more]({1}) about {2}.", defaultChat.privacyStatementUrl, defaultChat.documentationUrl, defaultChat.name); - this.element.appendChild($('p')).appendChild(this._register(markdown.render(new MarkdownString(footer, { isTrusted: true }))).element); - - // Update based on entilement changes - this._register(this.options.onDidChangeEntitlement(() => { - if (setupRunning) { - return; // do not change when setup running - } - setVisibility(this.options.entitlement !== ChatEntitlement.Unavailable, skuHeaderElement, telemetryContainer, detectionContainer); - this.updateControls(button, [telemetryCheckbox, detectionCheckbox], setupRunning); - })); + private registerListeners(): void { + this._register(this.entitlementResolver.onDidChangeEntitlement(() => this._onDidChange.fire())); } - private createCheckBox(label: string, checked: boolean): { container: HTMLElement; checkbox: Checkbox } { - const container = this.element.appendChild($('p')); - const checkbox = this._register(new Checkbox(label, checked, defaultCheckboxStyles)); - container.appendChild(checkbox.domNode); - - const telemetryCheckboxLabelContainer = container.appendChild($('div')); - telemetryCheckboxLabelContainer.textContent = label; - this._register(addDisposableListener(telemetryCheckboxLabelContainer, EventType.CLICK, () => { - if (checkbox?.enabled) { - checkbox.checked = !checkbox.checked; - } - })); + setStep(step: ChatSetupStep): void { + if (this._step === step) { + return; + } - return { container, checkbox }; + this._step = step; + this._onDidChange.fire(); } - private updateControls(button: Button, checkboxes: Checkbox[], setupRunning: boolean): void { - if (setupRunning) { - button.enabled = false; - button.label = localize('setupChatInstalling', "$(loading~spin) Completing Setup..."); - - for (const checkbox of checkboxes) { - checkbox.disable(); - } - } else { - button.enabled = true; - button.label = this.options.entitlement === ChatEntitlement.Unknown ? - localize('signInAndSetup', "Sign in and Complete Setup") : - localize('setup', "Complete Setup"); + async setup(enableTelemetry: boolean, enableDetection: boolean): Promise { + try { + let session: AuthenticationSession | undefined; + + // Entitlement Unknown: we need to sign-in user + if (this.entitlement === ChatEntitlement.Unknown) { + this.setStep(ChatSetupStep.SigningIn); + session = await this.signIn(); + if (!session) { + return; // user cancelled + } - for (const checkbox of checkboxes) { - checkbox.enable(); + const entitlement = await this.entitlementResolver.forceResolveEntitlement(session); + if (entitlement !== ChatEntitlement.Unavailable) { + return; // we cannot proceed with automated install because user needs to sign-up in a second step + } } - } - } - private async setup(enableTelemetry: boolean | undefined, enableDetection: boolean | undefined): Promise { - let session: AuthenticationSession | undefined; - if (this.options.entitlement === ChatEntitlement.Unknown) { - session = await this.signIn(); - if (!session) { - return false; // user cancelled - } + // Entitlement known: proceed with installation + this.setStep(ChatSetupStep.Installing); + await this.install(session, enableTelemetry, enableDetection); + } finally { + this.setStep(ChatSetupStep.Initial); } - - return this.install(session, enableTelemetry, enableDetection); } private async signIn(): Promise { @@ -475,7 +438,7 @@ class ChatSetupWelcomeContent extends Disposable { return session; } - private async install(session: AuthenticationSession | undefined, enableTelemetry: boolean | undefined, enableDetection: boolean | undefined): Promise { + private async install(session: AuthenticationSession | undefined, enableTelemetry: boolean, enableDetection: boolean): Promise { const signedIn = !!session; const activeElement = getActiveElement(); @@ -483,14 +446,14 @@ class ChatSetupWelcomeContent extends Disposable { try { showChatView(this.viewsService); - if (this.options.entitlement !== ChatEntitlement.Unavailable) { + if (this.canSignUpLimited) { const body = { - public_code_suggestions: enableDetection ? 'enabled' : 'disabled', - restricted_telemetry: enableTelemetry ? 'enabled' : 'disabled' + restricted_telemetry: enableTelemetry ? 'enabled' : 'disabled', + public_code_suggestions: enableDetection ? 'enabled' : 'disabled' }; this.logService.trace(`[chat setup] install: signing up to limited SKU with ${JSON.stringify(body)}`); - const response = await this.instantiationService.invokeFunction(accessor => ChatSetupRequestHelper.request(accessor, defaultChat.entitlementSkuLimitedUrl, 'POST', body, session, CancellationToken.None)); + const response = await this.instantiationService.invokeFunction(accessor => ChatSetupRequestHelper.request(accessor, defaultChat.entitlementSignupLimitedUrl, 'POST', body, session, CancellationToken.None)); if (response && this.logService.getLevel() === LogLevel.Trace) { this.logService.trace(`[chat setup] install: response from signing up to limited SKU ${JSON.stringify(await asText(response))}`); } @@ -516,8 +479,108 @@ class ChatSetupWelcomeContent extends Disposable { if (activeElement === getActiveElement()) { (await showChatView(this.viewsService))?.focusInput(); } + } +} + +class ChatSetupWelcomeContent extends Disposable { + + private static INSTANCE: ChatSetupWelcomeContent | undefined; + static getInstance(instantiationService: IInstantiationService, entitlementResolver: ChatSetupEntitlementResolver): ChatSetupWelcomeContent { + if (!ChatSetupWelcomeContent.INSTANCE) { + ChatSetupWelcomeContent.INSTANCE = instantiationService.createInstance(ChatSetupWelcomeContent, entitlementResolver); + } + + return ChatSetupWelcomeContent.INSTANCE; + } + + readonly element = $('.chat-setup-view'); + + private readonly controller: ChatSetupController; + + constructor( + entitlementResolver: ChatSetupEntitlementResolver, + @ITelemetryService private readonly telemetryService: ITelemetryService, + @IInstantiationService private readonly instantiationService: IInstantiationService + ) { + super(); + + this.controller = this._register(instantiationService.createInstance(ChatSetupController, entitlementResolver)); + + this.create(); + } + + private create(): void { + const markdown = this._register(this.instantiationService.createInstance(MarkdownRenderer, {})); + + // Header + this.element.appendChild($('p')).textContent = localize('setupHeader', "{0} is your AI pair programmer.", defaultChat.name); - return installResult === 'installed'; + // Limited SKU Sign-up + const limitedSkuHeader = localize({ key: 'limitedSkuHeader', comment: ['{Locked="]({0})"}'] }, "Setup will sign you up to {0} [limited access]({1}).", defaultChat.name, defaultChat.skusDocumentationUrl); + const limitedSkuHeaderElement = this.element.appendChild($('p')).appendChild(this._register(markdown.render(new MarkdownString(limitedSkuHeader, { isTrusted: true }))).element); + + const telemetryLabel = localize('telemetryLabel', "Allow {0} to use my data, including Prompts, Suggestions, and Code Snippets, for product improvements", defaultChat.providerName); + const { container: telemetryContainer, checkbox: telemetryCheckbox } = this.createCheckBox(telemetryLabel, this.telemetryService.telemetryLevel === TelemetryLevel.NONE ? false : false); + + const detectionLabel = localize('detectionLabel', "Allow suggestions matching public code"); + const { container: detectionContainer, checkbox: detectionCheckbox } = this.createCheckBox(detectionLabel, true); + + // Setup Button + const buttonRow = this.element.appendChild($('p')); + const button = this._register(new Button(buttonRow, { ...defaultButtonStyles, supportIcons: true })); + this._register(button.onDidClick(() => this.controller.setup(telemetryCheckbox.checked, detectionCheckbox.checked))); + + // Footer + const footer = localize({ key: 'privacyFooter', comment: ['{Locked="]({0})"}'] }, "By proceeding you agree to our [privacy statement]({0}). You can [learn more]({1}) about {2}.", defaultChat.privacyStatementUrl, defaultChat.documentationUrl, defaultChat.name); + this.element.appendChild($('p')).appendChild(this._register(markdown.render(new MarkdownString(footer, { isTrusted: true }))).element); + + // Update based on model state + this._register(Event.runAndSubscribe(this.controller.onDidChange, () => this.update([limitedSkuHeaderElement, telemetryContainer, detectionContainer], [telemetryCheckbox, detectionCheckbox], button))); + } + + private createCheckBox(label: string, checked: boolean): { container: HTMLElement; checkbox: Checkbox } { + const container = this.element.appendChild($('p')); + const checkbox = this._register(new Checkbox(label, checked, defaultCheckboxStyles)); + container.appendChild(checkbox.domNode); + + const checkboxLabel = container.appendChild($('div')); + checkboxLabel.textContent = label; + this._register(addDisposableListener(checkboxLabel, EventType.CLICK, () => { + if (checkbox?.enabled) { + checkbox.checked = !checkbox.checked; + } + })); + + return { container, checkbox }; + } + + private update(limitedContainers: HTMLElement[], limitedCheckboxes: Checkbox[], button: Button): void { + switch (this.controller.step) { + case ChatSetupStep.Initial: + setVisibility(this.controller.canSignUpLimited, ...limitedContainers); + + for (const checkbox of limitedCheckboxes) { + checkbox.enable(); + } + + button.enabled = true; + button.label = this.controller.entitlement === ChatEntitlement.Unknown ? + localize('signInToStartSetup', "Sign in to Start Setup") : + localize('startSetup', "Complete Setup"); + break; + case ChatSetupStep.SigningIn: + case ChatSetupStep.Installing: + for (const checkbox of limitedCheckboxes) { + checkbox.disable(); + } + + button.enabled = false; + button.label = this.controller.step === ChatSetupStep.SigningIn ? + localize('setupChatSigningIn', "$(loading~spin) Signing in to {0}...", defaultChat.providerName) : + localize('setupChatInstalling', "$(loading~spin) Completing Setup..."); + + break; + } } } @@ -559,14 +622,19 @@ class ChatSetupRequestHelper { } } -class ChatSetupState { +class ChatSetupContextKeys { private static readonly CHAT_SETUP_TRIGGERD = 'chat.setupTriggered'; private static readonly CHAT_EXTENSION_INSTALLED = 'chat.extensionInstalled'; + private readonly chatSetupEntitledContextKey = ChatContextKeys.Setup.entitled.bindTo(this.contextKeyService); + private readonly chatSetupLimitedContextKey = ChatContextKeys.Setup.limited.bindTo(this.contextKeyService); private readonly chatSetupTriggeredContext = ChatContextKeys.Setup.triggered.bindTo(this.contextKeyService); private readonly chatSetupInstalledContext = ChatContextKeys.Setup.installed.bindTo(this.contextKeyService); + private chatSetupEntitled = false; + private chatSetupLimited = false; + constructor( @IContextKeyService private readonly contextKeyService: IContextKeyService, @IStorageService private readonly storageService: IStorageService, @@ -575,28 +643,39 @@ class ChatSetupState { this.updateContext(); } + update(context: { chatInstalled: boolean }): void; update(context: { triggered: boolean }): void; - update(context: { chatInstalled?: boolean }): void; - update(context: { triggered?: boolean; chatInstalled?: boolean }): void { + update(context: { entitled: boolean; limited: boolean }): void; + update(context: { triggered?: boolean; chatInstalled?: boolean; entitled?: boolean; limited?: boolean }): void { if (typeof context.chatInstalled === 'boolean') { - this.storageService.store(ChatSetupState.CHAT_EXTENSION_INSTALLED, context.chatInstalled, StorageScope.PROFILE, StorageTarget.MACHINE); - this.storageService.store(ChatSetupState.CHAT_SETUP_TRIGGERD, true, StorageScope.PROFILE, StorageTarget.MACHINE); // allows to fallback to setup view if the extension is uninstalled + this.storageService.store(ChatSetupContextKeys.CHAT_EXTENSION_INSTALLED, context.chatInstalled, StorageScope.PROFILE, StorageTarget.MACHINE); + if (context.chatInstalled) { + this.storageService.store(ChatSetupContextKeys.CHAT_SETUP_TRIGGERD, true, StorageScope.PROFILE, StorageTarget.MACHINE); // allows to fallback to setup view if the extension is uninstalled + } } if (typeof context.triggered === 'boolean') { if (context.triggered) { - this.storageService.store(ChatSetupState.CHAT_SETUP_TRIGGERD, true, StorageScope.PROFILE, StorageTarget.MACHINE); + this.storageService.store(ChatSetupContextKeys.CHAT_SETUP_TRIGGERD, true, StorageScope.PROFILE, StorageTarget.MACHINE); } else { - this.storageService.remove(ChatSetupState.CHAT_SETUP_TRIGGERD, StorageScope.PROFILE); + this.storageService.remove(ChatSetupContextKeys.CHAT_SETUP_TRIGGERD, StorageScope.PROFILE); } } + if (typeof context.entitled === 'boolean') { + this.chatSetupEntitled = context.entitled; + } + + if (typeof context.limited === 'boolean') { + this.chatSetupLimited = context.limited; + } + this.updateContext(); } private updateContext(): void { - const chatSetupTriggered = this.storageService.getBoolean(ChatSetupState.CHAT_SETUP_TRIGGERD, StorageScope.PROFILE, false); - const chatInstalled = this.storageService.getBoolean(ChatSetupState.CHAT_EXTENSION_INSTALLED, StorageScope.PROFILE, false); + const chatSetupTriggered = this.storageService.getBoolean(ChatSetupContextKeys.CHAT_SETUP_TRIGGERD, StorageScope.PROFILE, false); + const chatInstalled = this.storageService.getBoolean(ChatSetupContextKeys.CHAT_EXTENSION_INSTALLED, StorageScope.PROFILE, false); const showChatSetup = chatSetupTriggered && !chatInstalled; if (showChatSetup) { @@ -607,6 +686,8 @@ class ChatSetupState { this.chatSetupTriggeredContext.set(showChatSetup); this.chatSetupInstalledContext.set(chatInstalled); + this.chatSetupEntitledContextKey.set(this.chatSetupEntitled); + this.chatSetupLimitedContextKey.set(this.chatSetupLimited); } } @@ -638,7 +719,7 @@ class ChatSetupTriggerAction extends Action2 { const viewsService = accessor.get(IViewsService); const instantiationService = accessor.get(IInstantiationService); - instantiationService.createInstance(ChatSetupState).update({ triggered: true }); + instantiationService.createInstance(ChatSetupContextKeys).update({ triggered: true }); showChatView(viewsService); } @@ -683,7 +764,7 @@ class ChatSetupHideAction extends Action2 { const location = viewsDescriptorService.getViewLocationById(ChatViewId); - instantiationService.createInstance(ChatSetupState).update({ triggered: false }); + instantiationService.createInstance(ChatSetupContextKeys).update({ triggered: false }); if (location === ViewContainerLocation.AuxiliaryBar) { const activeContainers = viewsDescriptorService.getViewContainersByLocation(location).filter(container => viewsDescriptorService.getViewContainerModel(container).activeViewDescriptors.length > 0); diff --git a/src/vs/workbench/contrib/chat/common/chatContextKeys.ts b/src/vs/workbench/contrib/chat/common/chatContextKeys.ts index 9e12fc95c2202..64493ef1734e0 100644 --- a/src/vs/workbench/contrib/chat/common/chatContextKeys.ts +++ b/src/vs/workbench/contrib/chat/common/chatContextKeys.ts @@ -42,7 +42,8 @@ export namespace ChatContextKeys { export const languageModelsAreUserSelectable = new RawContextKey('chatModelsAreUserSelectable', false, { type: 'boolean', description: localize('chatModelsAreUserSelectable', "True when the chat model can be selected manually by the user.") }); export const Setup = { - entitled: new RawContextKey('chatSetupEntitled', false, { type: 'boolean', description: localize('chatSetupEntitled', "True when chat setup is offered for a signed-in, entitled user.") }), + entitled: new RawContextKey('chatSetupEntitled', false, { type: 'boolean', description: localize('chatSetupEntitled', "True when user is a chat entitled user.") }), + limited: new RawContextKey('chatSetupLimited', false, { type: 'boolean', description: localize('chatSetupLimited', "True when user is a chat limited user.") }), triggered: new RawContextKey('chatSetupTriggered', false, { type: 'boolean', description: localize('chatSetupTriggered', "True when chat setup is triggered.") }), installed: new RawContextKey('chatSetupInstalled', false, { type: 'boolean', description: localize('chatSetupInstalled', "True when the chat extension is installed.") }), }; From 453c4410ae1a4e636146757a3dc602219e3e2f67 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Mon, 25 Nov 2024 09:22:02 +0100 Subject: [PATCH 087/119] Engineering - switch to variable group (#234537) * Engineering - switch to variable group * Fix typo --- build/azure-pipelines/product-build.yml | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/build/azure-pipelines/product-build.yml b/build/azure-pipelines/product-build.yml index d56ebc1cf1230..aa24a8f14db41 100644 --- a/build/azure-pipelines/product-build.yml +++ b/build/azure-pipelines/product-build.yml @@ -790,16 +790,12 @@ extends: name: 1es-ubuntu-22.04-x64 os: linux jobs: - - deployment: ApproveRelease + - job: ApproveRelease displayName: "Approve Release" - environment: "vscode" variables: - skipComponentGovernanceDetection: true - strategy: - runOnce: - deploy: - steps: - - checkout: none + - group: VSCode + - name: skipComponentGovernanceDetection + value: true - ${{ if or(and(parameters.VSCODE_RELEASE, eq(variables['VSCODE_PRIVATE_BUILD'], false)), and(in(parameters.VSCODE_QUALITY, 'insider', 'exploration'), eq(variables['VSCODE_SCHEDULEDBUILD'], true))) }}: - stage: Release From 1f4ce20cfd23dd2b484a0ca076641285e41d0d9d Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 25 Nov 2024 10:07:59 +0100 Subject: [PATCH 088/119] fix(chat): ensure entitlement is resolved only once and return entitlement value (#234540) --- .../chat/browser/chatSetup.contribution.ts | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts b/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts index bcd01d2344b2a..ebca64da21975 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts @@ -217,9 +217,10 @@ class ChatSetupEntitlementResolver extends Disposable { this.update(ChatEntitlement.Unknown); } - if (session) { + if (session && typeof this.resolvedEntitlement === 'undefined') { // Afterwards resolve entitlement with a network request - this.resolveEntitlement(session, cts.token); + // but only unless it was not already resolved before. + await this.resolveEntitlement(session, cts.token); } } @@ -244,16 +245,14 @@ class ChatSetupEntitlementResolver extends Disposable { return scopes.length === expectedScopes.length && expectedScopes.every(scope => scopes.includes(scope)); } - private async resolveEntitlement(session: AuthenticationSession, token: CancellationToken): Promise { - if (typeof this.resolvedEntitlement !== 'undefined') { - return; // only resolve once - } - + private async resolveEntitlement(session: AuthenticationSession, token: CancellationToken): Promise { const entitlement = await this.doResolveEntitlement(session, token); if (typeof entitlement === 'number' && !token.isCancellationRequested) { this.resolvedEntitlement = entitlement; this.update(entitlement); } + + return entitlement; } private async doResolveEntitlement(session: AuthenticationSession, token: CancellationToken): Promise { @@ -318,7 +317,7 @@ class ChatSetupEntitlementResolver extends Disposable { } async forceResolveEntitlement(session: AuthenticationSession): Promise { - return this.doResolveEntitlement(session, CancellationToken.None); + return this.resolveEntitlement(session, CancellationToken.None); } override dispose(): void { From b6a557c8628ae8a4db6b8e78800a5a9bb8a21c4a Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Mon, 25 Nov 2024 10:48:27 +0100 Subject: [PATCH 089/119] Clear excluded files from working set (#234542) --- .../contrib/chat/browser/chatAttachmentModel.ts | 2 +- .../chat/browser/chatEditing/chatEditingActions.ts | 10 ++++++++-- src/vs/workbench/contrib/chat/browser/chatInputPart.ts | 4 ++-- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatAttachmentModel.ts b/src/vs/workbench/contrib/chat/browser/chatAttachmentModel.ts index 3c0544720c6bf..06ca88ddb50c5 100644 --- a/src/vs/workbench/contrib/chat/browser/chatAttachmentModel.ts +++ b/src/vs/workbench/contrib/chat/browser/chatAttachmentModel.ts @@ -78,7 +78,7 @@ export class EditsAttachmentModel extends ChatAttachmentModel { private _onFileLimitExceeded = this._register(new Emitter()); readonly onFileLimitExceeded = this._onFileLimitExceeded.event; - private get fileAttachments() { + get fileAttachments() { return this.attachments.filter(attachment => attachment.isFile); } diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts index 05dfea4541b4c..491c01c1ba30d 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts @@ -26,6 +26,7 @@ import { IChatService } from '../../common/chatService.js'; import { isRequestVM, isResponseVM } from '../../common/chatViewModel.js'; import { CHAT_CATEGORY } from '../actions/chatActions.js'; import { ChatTreeItem, IChatWidget, IChatWidgetService } from '../chat.js'; +import { EditsAttachmentModel } from '../chatAttachmentModel.js'; abstract class WorkingSetAction extends Action2 { run(accessor: ServicesAccessor, ...args: any[]) { @@ -315,11 +316,16 @@ export class ChatEditingRemoveAllFilesAction extends Action2 { return; } + // Remove all files from working set const chatWidget = accessor.get(IChatWidgetService).getWidgetBySessionId(currentEditingSession.chatSessionId); const uris = [...currentEditingSession.workingSet.keys()]; currentEditingSession.remove(WorkingSetEntryRemovalReason.User, ...uris); - for (const uri of chatWidget?.attachmentModel.attachments ?? []) { - if (uri.isFile && URI.isUri(uri.value)) { + + // Remove all file attachments + const attachmentModel = chatWidget?.attachmentModel as EditsAttachmentModel | undefined; + const fileAttachments = attachmentModel ? [...attachmentModel.excludedFileAttachments, ...attachmentModel.fileAttachments] : []; + for (const uri of fileAttachments) { + if (URI.isUri(uri.value)) { chatWidget?.attachmentModel.delete(uri.value.toString()); } } diff --git a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts index 305c486651bed..9820f196b7884 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts @@ -1035,8 +1035,8 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge kind: 'reference', }; }) ?? []; - for (const attachment of this.attachmentModel.attachments) { - if (attachment.isFile && URI.isUri(attachment.value) && !seenEntries.has(attachment.value)) { + for (const attachment of (this.attachmentModel as EditsAttachmentModel).fileAttachments) { + if (URI.isUri(attachment.value) && !seenEntries.has(attachment.value)) { entries.unshift({ reference: attachment.value, state: WorkingSetEntryState.Attached, From c708e5897285b5e0b3dd225b66dfa675bdccb672 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Mon, 25 Nov 2024 11:15:03 +0100 Subject: [PATCH 090/119] tweak inline chat hint (#234544) --- .../lib/stylelint/vscode-known-variables.json | 1 + .../browser/inlineChatCurrentLine.ts | 33 ++++++++++++------- .../inlineChat/browser/media/inlineChat.css | 29 +++++++++++++++- 3 files changed, 50 insertions(+), 13 deletions(-) diff --git a/build/lib/stylelint/vscode-known-variables.json b/build/lib/stylelint/vscode-known-variables.json index c66764a97611b..a4b270551fff2 100644 --- a/build/lib/stylelint/vscode-known-variables.json +++ b/build/lib/stylelint/vscode-known-variables.json @@ -825,6 +825,7 @@ "--dropdown-padding-bottom", "--dropdown-padding-top", "--inline-chat-frame-progress", + "--inline-chat-hint-progress", "--insert-border-color", "--last-tab-margin-right", "--monaco-monospace-font", diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatCurrentLine.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatCurrentLine.ts index 78a792955d137..c7317928ad20d 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatCurrentLine.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatCurrentLine.ts @@ -27,6 +27,7 @@ import './media/inlineChat.css'; import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { InlineCompletionsController } from '../../../../editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.js'; +import { ChatAgentLocation, IChatAgentService } from '../../chat/common/chatAgents.js'; export const CTX_INLINE_CHAT_SHOWING_HINT = new RawContextKey('inlineChatShowingHint', false, localize('inlineChatShowingHint', "Whether inline chat shows a contextual hint")); @@ -154,6 +155,7 @@ export class InlineChatHintsController extends Disposable implements IEditorCont @IContextKeyService contextKeyService: IContextKeyService, @ICommandService commandService: ICommandService, @IKeybindingService keybindingService: IKeybindingService, + @IChatAgentService chatAgentService: IChatAgentService, ) { super(); this._editor = editor; @@ -179,44 +181,51 @@ export class InlineChatHintsController extends Disposable implements IEditorCont } })); - const posObs = observableFromEvent(editor.onDidChangeCursorPosition, () => editor.getPosition()); - const decos = this._editor.createDecorationsCollection(); + const modelObs = observableFromEvent(editor.onDidChangeModel, () => editor.getModel()); + const posObs = observableFromEvent(editor.onDidChangeCursorPosition, () => editor.getPosition()); const keyObs = observableFromEvent(keybindingService.onDidUpdateKeybindings, _ => keybindingService.lookupKeybinding(ACTION_START)?.getLabel()); - this._store.add(autorun(r => { const ghostState = ghostCtrl?.model.read(r)?.state.read(r); const visible = this._visibilityObs.read(r); const kb = keyObs.read(r); const position = posObs.read(r); + const model = modelObs.read(r); // update context key this._ctxShowingHint.set(visible); - if (!visible || !kb || !position || ghostState !== undefined) { + if (!visible || !kb || !position || ghostState !== undefined || !model) { decos.clear(); return; } - const column = this._editor.getModel()?.getLineMaxColumn(position.lineNumber); - if (!column) { - return; - } + const isEol = model.getLineMaxColumn(position.lineNumber) === position.column; - const range = Range.fromPositions(position); + let content: string; + let inlineClassName: string; + + if (isEol) { + const agentName = chatAgentService.getDefaultAgent(ChatAgentLocation.Editor)?.fullName ?? localize('defaultTitle', "Chat"); + content = '\u00A0' + localize('title', "{0} to continue with {1}...", kb, agentName); + inlineClassName = `inline-chat-hint${decos.length === 0 ? ' first' : ''}`; + } else { + content = '\u200a' + kb + '\u200a'; + inlineClassName = 'inline-chat-hint embedded'; + } decos.set([{ - range, + range: Range.fromPositions(position), options: { description: 'inline-chat-hint-line', showIfCollapsed: true, stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, after: { - inlineClassName: 'inline-chat-hint', - content: '\u00A0' + localize('ddd', "{0} to chat", kb), + content, + inlineClassName, inlineClassNameAffectsLetterSpacing: true, cursorStops: InjectedTextCursorStops.Both, attachedData: this diff --git a/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css index 5b10211a2420a..5cc3f844d1be5 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css +++ b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css @@ -340,10 +340,37 @@ /* HINT */ + .monaco-workbench .monaco-editor .inline-chat-hint { /* padding: 0 8px; */ cursor: pointer; color: var(--vscode-editorGhostText-foreground); - border: 1px solid var(--vscode-editorGhostText-border); + background-image: linear-gradient(45deg, var(--vscode-editorGhostText-foreground), 95%, transparent); + background-clip: text; + -webkit-text-fill-color: transparent; +} + +.monaco-workbench .monaco-editor .inline-chat-hint.embedded { + border: 1px solid var(--vscode-editorSuggestWidget-border); border-radius: 3px; } + +@property --inline-chat-hint-progress { + syntax: ''; + initial-value: 33%; + inherits: false; +} + +@keyframes ltr { + 0% { + --inline-chat-hint-progress: 33%; + } + 100% { + --inline-chat-hint-progress: 95%; + } +} + +.monaco-workbench .monaco-editor .inline-chat-hint.first { + background-image: linear-gradient(45deg, var(--vscode-editorGhostText-foreground), var(--inline-chat-hint-progress), transparent); + animation: 75ms ltr ease-in forwards; +} From c5a21581a0b2e68fc44264561f19be710261afe6 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Mon, 25 Nov 2024 11:17:51 +0100 Subject: [PATCH 091/119] use extensionVersion instead of version for reporting (#234545) --- .../extensionManagement/common/extensionManagementUtil.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/platform/extensionManagement/common/extensionManagementUtil.ts b/src/vs/platform/extensionManagement/common/extensionManagementUtil.ts index 9b0ef7b6ff419..39d008ba8be85 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagementUtil.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagementUtil.ts @@ -120,7 +120,7 @@ export function getLocalExtensionTelemetryData(extension: ILocalExtension) { "GalleryExtensionTelemetryData" : { "id" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "name": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "version": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "extensionVersion": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "galleryId": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "publisherId": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "publisherName": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, @@ -137,7 +137,7 @@ export function getGalleryExtensionTelemetryData(extension: IGalleryExtension) { return { id: new TelemetryTrustedValue(extension.identifier.id), name: new TelemetryTrustedValue(extension.name), - version: extension.version, + extensionVersion: extension.version, galleryId: extension.identifier.uuid, publisherId: extension.publisherId, publisherName: extension.publisher, From bb01685782e299058f41be996d6a8dc03e9a968f Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Mon, 25 Nov 2024 11:37:10 +0100 Subject: [PATCH 092/119] Fix drag-and-drop text and resource data for symbols (#234548) fix symbols dnd text data and resource data --- .../workbench/browser/parts/editor/breadcrumbsControl.ts | 7 ++++--- .../codeEditor/browser/outline/documentSymbolsTree.ts | 7 ++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts b/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts index 1b8d72b374517..9d72759aba2a9 100644 --- a/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts +++ b/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts @@ -112,9 +112,10 @@ class OutlineItem extends BreadcrumbsItem { }; const dataTransfers: DataTransfer[] = [ [CodeDataTransfers.SYMBOLS, [symbolTransferData]], - [DataTransfers.RESOURCES, [symbolUri]] + [DataTransfers.RESOURCES, [symbolUri.toString()]] ]; - this._disposables.add(createBreadcrumbDndObserver(container, element.symbol.name, symbolUri.toString(), dataTransfers)); + const textData = symbolUri.fsPath + (symbolUri.fragment ? '#' + symbolUri.fragment : ''); + this._disposables.add(createBreadcrumbDndObserver(container, element.symbol.name, textData, dataTransfers)); } } } @@ -163,7 +164,7 @@ class FileItem extends BreadcrumbsItem { [CodeDataTransfers.FILES, [this.element.uri.fsPath]], [DataTransfers.RESOURCES, [this.element.uri.toString()]], ]; - const dndObserver = createBreadcrumbDndObserver(container, basename(this.element.uri), this.element.uri.toString(), dataTransfers); + const dndObserver = createBreadcrumbDndObserver(container, basename(this.element.uri), this.element.uri.fsPath, dataTransfers); this._disposables.add(dndObserver); } } diff --git a/src/vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsTree.ts b/src/vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsTree.ts index d54288dde4b32..ce88a4be05695 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsTree.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsTree.ts @@ -76,9 +76,10 @@ export class DocumentSymbolDragAndDrop implements ITreeDragAndDrop symbolRangeUri(resource, oe.symbol)))); + originalEvent.dataTransfer.setData(DataTransfers.RESOURCES, JSON.stringify(outlineElements.map(oe => symbolRangeUri(resource, oe.symbol).toString()))); } onDragOver(): boolean | ITreeDragOverReaction { return false; } From 8be4a1414e1c9b6944925d84cedc545ab40494ad Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Mon, 25 Nov 2024 11:39:59 +0100 Subject: [PATCH 093/119] Fix folder drag and drop on macOS (#234550) Fix folder dnd on mac --- src/vs/workbench/contrib/chat/browser/chatDragAndDrop.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatDragAndDrop.ts b/src/vs/workbench/contrib/chat/browser/chatDragAndDrop.ts index 74f91ca5db7cc..7cfb2b8fb5dbf 100644 --- a/src/vs/workbench/contrib/chat/browser/chatDragAndDrop.ts +++ b/src/vs/workbench/contrib/chat/browser/chatDragAndDrop.ts @@ -164,7 +164,7 @@ export class ChatDragAndDrop extends Themable { return ChatDragAndDropType.FILE_EXTERNAL; } else if (containsDragType(e, DataTransfers.INTERNAL_URI_LIST)) { return ChatDragAndDropType.FILE_INTERNAL; - } else if (containsDragType(e, Mimes.uriList)) { + } else if (containsDragType(e, Mimes.uriList, CodeDataTransfers.FILES)) { return ChatDragAndDropType.FOLDER; } From 1ff9676e4860a0f5335b7b1d44d22434509674ba Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Mon, 25 Nov 2024 11:40:53 +0100 Subject: [PATCH 094/119] add missing tooltip (#234551) --- .../contrib/inlineChat/browser/inlineChatCurrentLine.ts | 4 +++- .../workbench/contrib/inlineChat/browser/media/inlineChat.css | 2 -- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatCurrentLine.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatCurrentLine.ts index c7317928ad20d..e1b70cbbc8334 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatCurrentLine.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatCurrentLine.ts @@ -28,6 +28,7 @@ import { IKeybindingService } from '../../../../platform/keybinding/common/keybi import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { InlineCompletionsController } from '../../../../editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.js'; import { ChatAgentLocation, IChatAgentService } from '../../chat/common/chatAgents.js'; +import { MarkdownString } from '../../../../base/common/htmlContent.js'; export const CTX_INLINE_CHAT_SHOWING_HINT = new RawContextKey('inlineChatShowingHint', false, localize('inlineChatShowingHint', "Whether inline chat shows a contextual hint")); @@ -203,13 +204,13 @@ export class InlineChatHintsController extends Disposable implements IEditorCont return; } + const agentName = chatAgentService.getDefaultAgent(ChatAgentLocation.Editor)?.fullName ?? localize('defaultTitle', "Chat"); const isEol = model.getLineMaxColumn(position.lineNumber) === position.column; let content: string; let inlineClassName: string; if (isEol) { - const agentName = chatAgentService.getDefaultAgent(ChatAgentLocation.Editor)?.fullName ?? localize('defaultTitle', "Chat"); content = '\u00A0' + localize('title', "{0} to continue with {1}...", kb, agentName); inlineClassName = `inline-chat-hint${decos.length === 0 ? ' first' : ''}`; } else { @@ -223,6 +224,7 @@ export class InlineChatHintsController extends Disposable implements IEditorCont description: 'inline-chat-hint-line', showIfCollapsed: true, stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, + hoverMessage: new MarkdownString(localize('toolttip', "Continue this with {0}...", agentName)), after: { content, inlineClassName, diff --git a/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css index 5cc3f844d1be5..9cb1eccb91dd4 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css +++ b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css @@ -340,9 +340,7 @@ /* HINT */ - .monaco-workbench .monaco-editor .inline-chat-hint { - /* padding: 0 8px; */ cursor: pointer; color: var(--vscode-editorGhostText-foreground); background-image: linear-gradient(45deg, var(--vscode-editorGhostText-foreground), 95%, transparent); From c4d69296a280d5a78966816d9289345f085b9843 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 25 Nov 2024 12:31:53 +0100 Subject: [PATCH 095/119] chat - tweaks to welcome (#234556) * chat - tweaks to welcome * . * . * . * . * . * . * . * . --- src/vs/base/common/product.ts | 1 - .../chat/browser/actions/chatActions.ts | 19 ++++++++++---- .../contrib/chat/browser/chat.contribution.ts | 3 ++- .../chat/browser/chatSetup.contribution.ts | 26 +++++++++++-------- .../chat/browser/media/chatViewSetup.css | 9 +++++++ 5 files changed, 40 insertions(+), 18 deletions(-) diff --git a/src/vs/base/common/product.ts b/src/vs/base/common/product.ts index c5ad8ec863bb3..4ef6a92946ac9 100644 --- a/src/vs/base/common/product.ts +++ b/src/vs/base/common/product.ts @@ -312,7 +312,6 @@ export interface IDefaultChatAgent { readonly chatWelcomeTitle: string; readonly documentationUrl: string; readonly privacyStatementUrl: string; - readonly collectionDocumentationUrl: string; readonly skusDocumentationUrl: string; readonly providerId: string; readonly providerName: string; diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts index 9ee591e5fc2d0..e32c8ad24b127 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts @@ -470,11 +470,20 @@ export function registerChatActions() { id: LearnMoreChatAction.ID, title: LearnMoreChatAction.TITLE, category: CHAT_CATEGORY, - menu: { - id: MenuId.ChatCommandCenter, - group: 'z_learn', - order: 1 - } + menu: [ + { + id: MenuId.ChatCommandCenter, + group: 'z_learn', + order: 1, + when: ChatContextKeys.Setup.installed + }, + { + id: MenuId.ChatCommandCenter, + group: 'a_first', + order: 2, + when: ChatContextKeys.Setup.installed.toNegated() + } + ] }); } diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 50db228ffcd08..bbf092cdf72e2 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -79,6 +79,7 @@ import { ChatGettingStartedContribution } from './actions/chatGettingStarted.js' import { Extensions, IConfigurationMigrationRegistry } from '../../../common/configuration.js'; import { ChatEditorOverlayController } from './chatEditorOverlay.js'; import { ChatRelatedFilesContribution } from './contrib/chatInputRelatedFilesContrib.js'; +import product from '../../../../platform/product/common/product.js'; // Register configuration const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); @@ -116,7 +117,7 @@ configurationRegistry.registerConfiguration({ 'chat.commandCenter.enabled': { type: 'boolean', tags: ['preview'], - markdownDescription: nls.localize('chat.commandCenter.enabled', "Controls whether the command center shows a menu for chat actions (requires {0}).", '`#window.commandCenter#`'), + markdownDescription: nls.localize('chat.commandCenter.enabled', "Controls whether the command center shows a menu for chat actions to control {0} (requires {1}).", product.defaultChatAgent?.chatName, '`#window.commandCenter#`'), default: true }, 'chat.experimental.offerSetup': { diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts b/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts index ebca64da21975..32f7a253579d0 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts @@ -46,12 +46,12 @@ import { MarkdownString } from '../../../../base/common/htmlContent.js'; const defaultChat = { extensionId: product.defaultChatAgent?.extensionId ?? '', + chatExtensionId: product.defaultChatAgent?.chatExtensionId ?? '', name: product.defaultChatAgent?.name ?? '', icon: Codicon[product.defaultChatAgent?.icon as keyof typeof Codicon ?? 'commentDiscussion'], chatWelcomeTitle: product.defaultChatAgent?.chatWelcomeTitle ?? '', documentationUrl: product.defaultChatAgent?.documentationUrl ?? '', privacyStatementUrl: product.defaultChatAgent?.privacyStatementUrl ?? '', - collectionDocumentationUrl: product.defaultChatAgent?.collectionDocumentationUrl ?? '', skusDocumentationUrl: product.defaultChatAgent?.skusDocumentationUrl ?? '', providerId: product.defaultChatAgent?.providerId ?? '', providerName: product.defaultChatAgent?.providerName ?? '', @@ -468,7 +468,7 @@ class ChatSetupController extends Disposable { installResult = 'installed'; } catch (error) { - this.logService.trace(`[chat setup] install: error ${error}`); + this.logService.error(`[chat setup] install: error ${error}`); installResult = isCancellationError(error) ? 'cancelled' : 'failedInstall'; } @@ -519,7 +519,7 @@ class ChatSetupWelcomeContent extends Disposable { const limitedSkuHeaderElement = this.element.appendChild($('p')).appendChild(this._register(markdown.render(new MarkdownString(limitedSkuHeader, { isTrusted: true }))).element); const telemetryLabel = localize('telemetryLabel', "Allow {0} to use my data, including Prompts, Suggestions, and Code Snippets, for product improvements", defaultChat.providerName); - const { container: telemetryContainer, checkbox: telemetryCheckbox } = this.createCheckBox(telemetryLabel, this.telemetryService.telemetryLevel === TelemetryLevel.NONE ? false : false); + const { container: telemetryContainer, checkbox: telemetryCheckbox } = this.createCheckBox(telemetryLabel, this.telemetryService.telemetryLevel === TelemetryLevel.NONE ? false : true); const detectionLabel = localize('detectionLabel', "Allow suggestions matching public code"); const { container: detectionContainer, checkbox: detectionCheckbox } = this.createCheckBox(detectionLabel, true); @@ -530,7 +530,7 @@ class ChatSetupWelcomeContent extends Disposable { this._register(button.onDidClick(() => this.controller.setup(telemetryCheckbox.checked, detectionCheckbox.checked))); // Footer - const footer = localize({ key: 'privacyFooter', comment: ['{Locked="]({0})"}'] }, "By proceeding you agree to our [privacy statement]({0}). You can [learn more]({1}) about {2}.", defaultChat.privacyStatementUrl, defaultChat.documentationUrl, defaultChat.name); + const footer = localize({ key: 'privacyFooter', comment: ['{Locked="]({0})"}'] }, "By proceeding you agree to our [privacy statement]({0}). Click [here]({1}) to learn more about {2}.", defaultChat.privacyStatementUrl, defaultChat.documentationUrl, defaultChat.name); this.element.appendChild($('p')).appendChild(this._register(markdown.render(new MarkdownString(footer, { isTrusted: true }))).element); // Update based on model state @@ -538,15 +538,16 @@ class ChatSetupWelcomeContent extends Disposable { } private createCheckBox(label: string, checked: boolean): { container: HTMLElement; checkbox: Checkbox } { - const container = this.element.appendChild($('p')); + const container = this.element.appendChild($('p.checkbox-container')); const checkbox = this._register(new Checkbox(label, checked, defaultCheckboxStyles)); container.appendChild(checkbox.domNode); - const checkboxLabel = container.appendChild($('div')); + const checkboxLabel = container.appendChild($('div.checkbox-label')); checkboxLabel.textContent = label; this._register(addDisposableListener(checkboxLabel, EventType.CLICK, () => { if (checkbox?.enabled) { checkbox.checked = !checkbox.checked; + checkbox.focus(); } })); @@ -717,17 +718,20 @@ class ChatSetupTriggerAction extends Action2 { override async run(accessor: ServicesAccessor): Promise { const viewsService = accessor.get(IViewsService); const instantiationService = accessor.get(IInstantiationService); + const configurationService = accessor.get(IConfigurationService); instantiationService.createInstance(ChatSetupContextKeys).update({ triggered: true }); showChatView(viewsService); + + configurationService.updateValue('chat.commandCenter.enabled', true); } } class ChatSetupHideAction extends Action2 { static readonly ID = 'workbench.action.chat.hideSetup'; - static readonly TITLE = localize2('hideChatSetup', "Hide {0}", defaultChat.name); + static readonly TITLE = localize2('hideChatSetup', "Hide {0}...", defaultChat.name); constructor() { super({ @@ -737,8 +741,8 @@ class ChatSetupHideAction extends Action2 { precondition: ChatContextKeys.Setup.installed.negate(), menu: { id: MenuId.ChatCommandCenter, - group: 'a_first', - order: 2, + group: 'z_hide', + order: 1, when: ChatContextKeys.Setup.installed.negate() } }); @@ -753,8 +757,8 @@ class ChatSetupHideAction extends Action2 { const { confirmed } = await dialogService.confirm({ message: localize('hideChatSetupConfirm', "Are you sure you want to hide {0}?", defaultChat.name), - detail: localize('hideChatSetupDetail', "You can restore chat controls from the 'chat.commandCenter.enabled' setting."), - primaryButton: localize('hideChatSetup', "Hide {0}", defaultChat.name) + detail: localize('hideChatSetupDetail', "You can restore it by running the '{0}' command.", ChatSetupTriggerAction.TITLE.value), + primaryButton: localize('hideChatSetupButton', "Hide {0}", defaultChat.name) }); if (!confirmed) { diff --git a/src/vs/workbench/contrib/chat/browser/media/chatViewSetup.css b/src/vs/workbench/contrib/chat/browser/media/chatViewSetup.css index 988c082bdc611..485f7e13a8c13 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chatViewSetup.css +++ b/src/vs/workbench/contrib/chat/browser/media/chatViewSetup.css @@ -22,4 +22,13 @@ width: 100%; padding: 4px 7px; } + + p.checkbox-container { + display: flex; + } + + div.checkbox-label { + flex-basis: fit-content; + cursor: pointer; + } } From 66b424ce9dedfe18b58de5d9126b5426b6a3e6a3 Mon Sep 17 00:00:00 2001 From: Christof Marti Date: Mon, 25 Nov 2024 12:36:00 +0100 Subject: [PATCH 096/119] Fix kerberos import (github/copilot#13764) --- src/vs/platform/request/node/requestService.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/vs/platform/request/node/requestService.ts b/src/vs/platform/request/node/requestService.ts index 010acda05469b..a9db3e2d6a3a9 100644 --- a/src/vs/platform/request/node/requestService.ts +++ b/src/vs/platform/request/node/requestService.ts @@ -115,7 +115,8 @@ export class RequestService extends AbstractRequestService implements IRequestSe async lookupKerberosAuthorization(urlStr: string): Promise { try { - const kerberos = await import('kerberos'); + const importKerberos = await import('kerberos'); + const kerberos = importKerberos.default || importKerberos; const url = new URL(urlStr); const spn = this.configurationService.getValue('http.proxyKerberosServicePrincipal') || (process.platform === 'win32' ? `HTTP/${url.hostname}` : `HTTP@${url.hostname}`); From 6eaf3597ec6bb6520af0256ad81e5e3a77710393 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Mon, 25 Nov 2024 13:02:13 +0100 Subject: [PATCH 097/119] Engineering - update variable group name (#234561) --- build/azure-pipelines/product-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/azure-pipelines/product-build.yml b/build/azure-pipelines/product-build.yml index aa24a8f14db41..39075d822831b 100644 --- a/build/azure-pipelines/product-build.yml +++ b/build/azure-pipelines/product-build.yml @@ -793,7 +793,7 @@ extends: - job: ApproveRelease displayName: "Approve Release" variables: - - group: VSCode + - group: VSCodePeerApproval - name: skipComponentGovernanceDetection value: true From 69d0a639d34f3f7c1a69cfda8426a92710b76bd1 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 25 Nov 2024 13:26:26 +0100 Subject: [PATCH 098/119] chat - tweaks to welcome (#234560) --- package.json | 2 +- .../chat/browser/chatSetup.contribution.ts | 47 ++++++++++++++++--- 2 files changed, 42 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 5fa4e37886f80..3a65bba50c9a2 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.96.0", - "distro": "b923ae4b113ee415bd170b90e6d8500a76516360", + "distro": "6adce63c9c185e5d9c557a4cc170a363db22c1ee", "author": { "name": "Microsoft Corporation" }, diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts b/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts index 32f7a253579d0..30b87176a773e 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts @@ -43,6 +43,9 @@ import { defaultButtonStyles, defaultCheckboxStyles } from '../../../../platform import { Button } from '../../../../base/browser/ui/button/button.js'; import { MarkdownRenderer } from '../../../../editor/browser/widget/markdownRenderer/browser/markdownRenderer.js'; import { MarkdownString } from '../../../../base/common/htmlContent.js'; +import { IProgressService, ProgressLocation } from '../../../../platform/progress/common/progress.js'; +import { Barrier, timeout } from '../../../../base/common/async.js'; +import { IChatAgentService } from '../common/chatAgents.js'; const defaultChat = { extensionId: product.defaultChatAgent?.extensionId ?? '', @@ -107,7 +110,7 @@ class ChatSetupContribution extends Disposable implements IWorkbenchContribution ChatContextKeys.Setup.installed.negate() )!, icon: defaultChat.icon, - content: () => ChatSetupWelcomeContent.getInstance(this.instantiationService, this.entitlementsResolver).element, + content: () => ChatSetupWelcomeContent.getInstance(this.instantiationService, this.entitlementsResolver, this.chatSetupContextKeys).element, }); } @@ -369,13 +372,16 @@ class ChatSetupController extends Disposable { constructor( private readonly entitlementResolver: ChatSetupEntitlementResolver, + private readonly chatSetupContextKeys: ChatSetupContextKeys, @ITelemetryService private readonly telemetryService: ITelemetryService, @IAuthenticationService private readonly authenticationService: IAuthenticationService, @IInstantiationService private readonly instantiationService: IInstantiationService, @IViewsService private readonly viewsService: IViewsService, @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, @IProductService private readonly productService: IProductService, - @ILogService private readonly logService: ILogService + @ILogService private readonly logService: ILogService, + @IProgressService private readonly progressService: IProgressService, + @IChatAgentService private readonly chatAgentService: IChatAgentService ) { super(); @@ -396,6 +402,14 @@ class ChatSetupController extends Disposable { } async setup(enableTelemetry: boolean, enableDetection: boolean): Promise { + return this.progressService.withProgress({ + location: ProgressLocation.Window, + command: ChatSetupTriggerAction.ID, + title: localize('setupChatProgress', "Setting up {0}...", defaultChat.name), + }, () => this.doSetup(enableTelemetry, enableDetection)); + } + + private async doSetup(enableTelemetry: boolean, enableDetection: boolean): Promise { try { let session: AuthenticationSession | undefined; @@ -460,6 +474,8 @@ class ChatSetupController extends Disposable { this.logService.trace('[chat setup] install: not signing up to limited SKU'); } + this.chatSetupContextKeys.suspend(); // reduces flicker + await this.extensionsWorkbenchService.install(defaultChat.extensionId, { enable: true, isMachineScoped: false, @@ -471,6 +487,11 @@ class ChatSetupController extends Disposable { this.logService.error(`[chat setup] install: error ${error}`); installResult = isCancellationError(error) ? 'cancelled' : 'failedInstall'; + } finally { + await Promise.race([ + timeout(5000), // helps prevent flicker with sign-in welcome view + Event.toPromise(this.chatAgentService.onDidChangeAgents) // https://github.com/microsoft/vscode-copilot/issues/9274 + ]).finally(() => this.chatSetupContextKeys.resume()); } this.telemetryService.publicLog2('commandCenter.chatInstall', { installResult, signedIn }); @@ -484,9 +505,9 @@ class ChatSetupController extends Disposable { class ChatSetupWelcomeContent extends Disposable { private static INSTANCE: ChatSetupWelcomeContent | undefined; - static getInstance(instantiationService: IInstantiationService, entitlementResolver: ChatSetupEntitlementResolver): ChatSetupWelcomeContent { + static getInstance(instantiationService: IInstantiationService, entitlementResolver: ChatSetupEntitlementResolver, chatSetupContextKeys: ChatSetupContextKeys): ChatSetupWelcomeContent { if (!ChatSetupWelcomeContent.INSTANCE) { - ChatSetupWelcomeContent.INSTANCE = instantiationService.createInstance(ChatSetupWelcomeContent, entitlementResolver); + ChatSetupWelcomeContent.INSTANCE = instantiationService.createInstance(ChatSetupWelcomeContent, entitlementResolver, chatSetupContextKeys); } return ChatSetupWelcomeContent.INSTANCE; @@ -498,12 +519,13 @@ class ChatSetupWelcomeContent extends Disposable { constructor( entitlementResolver: ChatSetupEntitlementResolver, + chatSetupContextKeys: ChatSetupContextKeys, @ITelemetryService private readonly telemetryService: ITelemetryService, @IInstantiationService private readonly instantiationService: IInstantiationService ) { super(); - this.controller = this._register(instantiationService.createInstance(ChatSetupController, entitlementResolver)); + this.controller = this._register(instantiationService.createInstance(ChatSetupController, entitlementResolver, chatSetupContextKeys)); this.create(); } @@ -635,6 +657,8 @@ class ChatSetupContextKeys { private chatSetupEntitled = false; private chatSetupLimited = false; + private contextKeyUpdateBarrier: Barrier | undefined = undefined; + constructor( @IContextKeyService private readonly contextKeyService: IContextKeyService, @IStorageService private readonly storageService: IStorageService, @@ -673,7 +697,9 @@ class ChatSetupContextKeys { this.updateContext(); } - private updateContext(): void { + private async updateContext(): Promise { + await this.contextKeyUpdateBarrier?.wait(); + const chatSetupTriggered = this.storageService.getBoolean(ChatSetupContextKeys.CHAT_SETUP_TRIGGERD, StorageScope.PROFILE, false); const chatInstalled = this.storageService.getBoolean(ChatSetupContextKeys.CHAT_EXTENSION_INSTALLED, StorageScope.PROFILE, false); @@ -689,6 +715,15 @@ class ChatSetupContextKeys { this.chatSetupEntitledContextKey.set(this.chatSetupEntitled); this.chatSetupLimitedContextKey.set(this.chatSetupLimited); } + + suspend(): void { + this.contextKeyUpdateBarrier = new Barrier(); + } + + resume(): void { + this.contextKeyUpdateBarrier?.open(); + this.contextKeyUpdateBarrier = undefined; + } } //#endregion From 15a6c45aa71a928ee4b0b850b6c8d996a072c026 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Mon, 25 Nov 2024 13:27:15 +0100 Subject: [PATCH 099/119] Support dragging untitled editors into chat (#234562) Tmp solution for untitled editors dnd --- .../contrib/chat/browser/chatDragAndDrop.ts | 36 +++++++++++++++++-- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatDragAndDrop.ts b/src/vs/workbench/contrib/chat/browser/chatDragAndDrop.ts index 7cfb2b8fb5dbf..519b7c7737397 100644 --- a/src/vs/workbench/contrib/chat/browser/chatDragAndDrop.ts +++ b/src/vs/workbench/contrib/chat/browser/chatDragAndDrop.ts @@ -18,8 +18,11 @@ import { localize } from '../../../../nls.js'; import { CodeDataTransfers, containsDragType, DocumentSymbolTransferData, extractEditorsDropData, extractSymbolDropData, IDraggedResourceEditorInput } from '../../../../platform/dnd/browser/dnd.js'; import { FileType, IFileService, IFileSystemProvider } from '../../../../platform/files/common/files.js'; import { IThemeService, Themable } from '../../../../platform/theme/common/themeService.js'; +import { isUntitledResourceEditorInput } from '../../../common/editor.js'; import { EditorInput } from '../../../common/editor/editorInput.js'; +import { IEditorService } from '../../../services/editor/common/editorService.js'; import { IExtensionService, isProposedApiEnabled } from '../../../services/extensions/common/extensions.js'; +import { UntitledTextEditorInput } from '../../../services/untitled/common/untitledTextEditorInput.js'; import { IChatRequestVariableEntry } from '../common/chatModel.js'; import { ChatAttachmentModel } from './chatAttachmentModel.js'; import { IChatInputStyles } from './chatInputPart.js'; @@ -43,7 +46,8 @@ export class ChatDragAndDrop extends Themable { private readonly styles: IChatInputStyles, @IThemeService themeService: IThemeService, @IExtensionService private readonly extensionService: IExtensionService, - @IFileService protected readonly fileService: IFileService + @IFileService protected readonly fileService: IFileService, + @IEditorService protected readonly editorService: IEditorService, ) { super(themeService); @@ -238,6 +242,12 @@ export class ChatDragAndDrop extends Themable { } private async getEditorAttachContext(editor: EditorInput | IDraggedResourceEditorInput): Promise { + + // untitled editor + if (isUntitledResourceEditorInput(editor)) { + return await this.resolveUntitledAttachContext(editor); + } + if (!editor.resource) { return undefined; } @@ -256,6 +266,25 @@ export class ChatDragAndDrop extends Themable { return getResourceAttachContext(editor.resource, stat.isDirectory); } + private async resolveUntitledAttachContext(editor: IDraggedResourceEditorInput): Promise { + // If the resource is known, we can use it directly + if (editor.resource) { + return getResourceAttachContext(editor.resource, false); + } + + // Otherwise, we need to check if the contents are already open in another editor + const openUntitledEditors = this.editorService.editors.filter(editor => editor instanceof UntitledTextEditorInput) as UntitledTextEditorInput[]; + for (const canidate of openUntitledEditors) { + const model = await canidate.resolve(); + const contents = model.textEditorModel?.getValue(); + if (contents === editor.contents) { + return getResourceAttachContext(canidate.resource, false); + } + } + + return undefined; + } + private resolveSymbolsAttachContext(symbols: DocumentSymbolTransferData[]): IChatRequestVariableEntry[] { return symbols.map(symbol => { const resource = URI.file(symbol.fsPath); @@ -318,9 +347,10 @@ export class EditsDragAndDrop extends ChatDragAndDrop { styles: IChatInputStyles, @IThemeService themeService: IThemeService, @IExtensionService extensionService: IExtensionService, - @IFileService fileService: IFileService + @IFileService fileService: IFileService, + @IEditorService editorService: IEditorService, ) { - super(attachmentModel, styles, themeService, extensionService, fileService); + super(attachmentModel, styles, themeService, extensionService, fileService, editorService); } protected override handleDrop(context: IChatRequestVariableEntry[]): void { From 1d2ddbac0284b6c51e5a7b0e27f114de14c0a46f Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Mon, 25 Nov 2024 14:10:58 +0100 Subject: [PATCH 100/119] change navigation depends on currently navigated to value, not selection (#234559) * fix an issue with navigating changed * change navigation depends on currently navigated to value, not selection re https://github.com/microsoft/vscode-copilot/issues/10682 --- .../contrib/chat/browser/chatEditorActions.ts | 10 +- .../chat/browser/chatEditorController.ts | 91 +++++++++++-------- .../contrib/chat/browser/chatEditorOverlay.ts | 12 +-- 3 files changed, 64 insertions(+), 49 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatEditorActions.ts b/src/vs/workbench/contrib/chat/browser/chatEditorActions.ts index c782bfcd5c8c8..16aa746bdd36b 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditorActions.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditorActions.ts @@ -49,7 +49,7 @@ abstract class NavigateAction extends Action2 { }); } - override run(accessor: ServicesAccessor) { + override async run(accessor: ServicesAccessor) { const chatEditingService = accessor.get(IChatEditingService); const editorService = accessor.get(IEditorService); @@ -97,7 +97,7 @@ abstract class NavigateAction extends Action2 { const entry = entries[newIdx]; const change = entry.diffInfo.get().changes.at(this.next ? 0 : -1); - return editorService.openEditor({ + const newEditorPane = await editorService.openEditor({ resource: entry.modifiedURI, options: { selection: change && Range.fromPositions({ lineNumber: change.original.startLineNumber, column: 1 }), @@ -105,6 +105,12 @@ abstract class NavigateAction extends Action2 { revealIfVisible: false, } }, ACTIVE_GROUP); + + + const newEditor = newEditorPane?.getControl(); + if (isCodeEditor(newEditor)) { + ChatEditorController.get(newEditor)?.initNavigation(); + } } } diff --git a/src/vs/workbench/contrib/chat/browser/chatEditorController.ts b/src/vs/workbench/contrib/chat/browser/chatEditorController.ts index fb684a083b59d..3a65b8f701376 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditorController.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditorController.ts @@ -5,9 +5,8 @@ import './media/chatEditorController.css'; import { getTotalWidth } from '../../../../base/browser/dom.js'; -import { binarySearch, coalesceInPlace } from '../../../../base/common/arrays.js'; import { Disposable, DisposableStore, dispose, toDisposable } from '../../../../base/common/lifecycle.js'; -import { autorun, derived, observableFromEvent } from '../../../../base/common/observable.js'; +import { autorun, derived, IObservable, observableFromEvent, observableValue } from '../../../../base/common/observable.js'; import { isEqual } from '../../../../base/common/resources.js'; import { themeColorFromId } from '../../../../base/common/themables.js'; import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition, IOverlayWidgetPositionCoordinates, IViewZone, MouseTargetType } from '../../../../editor/browser/editorBrowser.js'; @@ -39,7 +38,10 @@ export class ChatEditorController extends Disposable implements IEditorContribut public static readonly ID = 'editor.contrib.chatEditorController'; - private readonly _decorations = this._editor.createDecorationsCollection(); + private static _diffLineDecorationData = ModelDecorationOptions.register({ description: 'diff-line-decoration' }); + + private readonly _diffLineDecorations = this._editor.createDecorationsCollection(); // tracks the line range w/o visuals (used for navigate) + private readonly _diffVisualDecorations = this._editor.createDecorationsCollection(); // tracks the real diff with character level inserts private readonly _diffHunksRenderStore = this._register(new DisposableStore()); private readonly _diffHunkWidgets: DiffHunkWidget[] = []; @@ -51,6 +53,9 @@ export class ChatEditorController extends Disposable implements IEditorContribut return controller; } + private readonly _currentChange = observableValue(this, undefined); + readonly currentChange: IObservable = this._currentChange; + constructor( private readonly _editor: ICodeEditor, @IInstantiationService private readonly _instantiationService: IInstantiationService, @@ -90,6 +95,7 @@ export class ChatEditorController extends Disposable implements IEditorContribut const diff = entry?.diffInfo.read(r); this._updateWithDiff(entry, diff); + this.revealNext(); })); const shouldBeReadOnly = derived(this, r => { @@ -140,7 +146,8 @@ export class ChatEditorController extends Disposable implements IEditorContribut }); this._viewZones = []; this._diffHunksRenderStore.clear(); - this._decorations.clear(); + this._diffVisualDecorations.clear(); + this._diffLineDecorations.clear(); this._ctxHasEditorModification.reset(); } @@ -181,13 +188,15 @@ export class ChatEditorController extends Disposable implements IEditorContribut viewZoneChangeAccessor.removeZone(id); } this._viewZones = []; - const modifiedDecorations: IModelDeltaDecoration[] = []; + const modifiedVisualDecorations: IModelDeltaDecoration[] = []; + const modifiedLineDecorations: IModelDeltaDecoration[] = []; const mightContainNonBasicASCII = originalModel.mightContainNonBasicASCII(); const mightContainRTL = originalModel.mightContainRTL(); const renderOptions = RenderOptions.fromEditor(this._editor); const editorLineCount = this._editor.getModel()?.getLineCount(); for (const diffEntry of diff.changes) { + const originalRange = diffEntry.original; originalModel.tokenization.forceTokenization(Math.max(1, originalRange.endLineNumberExclusive - 1)); const source = new LineSource( @@ -206,7 +215,7 @@ export class ChatEditorController extends Disposable implements IEditorContribut // If the original range is empty, the start line number is 1 and the new range spans the entire file, don't draw an Added decoration if (!(i.originalRange.isEmpty() && i.originalRange.startLineNumber === 1 && i.modifiedRange.endLineNumber === editorLineCount) && !i.modifiedRange.isEmpty()) { - modifiedDecorations.push({ + modifiedVisualDecorations.push({ range: i.modifiedRange, options: chatDiffAddDecoration }); } @@ -217,7 +226,7 @@ export class ChatEditorController extends Disposable implements IEditorContribut const isCreatedContent = decorations.length === 1 && decorations[0].range.isEmpty() && diffEntry.original.startLineNumber === 1; if (!diffEntry.modified.isEmpty && !(isCreatedContent && (diffEntry.modified.endLineNumberExclusive - 1) === editorLineCount)) { - modifiedDecorations.push({ + modifiedVisualDecorations.push({ range: diffEntry.modified.toInclusiveRange()!, options: chatDiffWholeLineAddDecoration }); @@ -225,19 +234,19 @@ export class ChatEditorController extends Disposable implements IEditorContribut if (diffEntry.original.isEmpty) { // insertion - modifiedDecorations.push({ + modifiedVisualDecorations.push({ range: diffEntry.modified.toInclusiveRange()!, options: addedDecoration }); } else if (diffEntry.modified.isEmpty) { // deletion - modifiedDecorations.push({ + modifiedVisualDecorations.push({ range: new Range(diffEntry.modified.startLineNumber - 1, 1, diffEntry.modified.startLineNumber, 1), options: deletedDecoration }); } else { // modification - modifiedDecorations.push({ + modifiedVisualDecorations.push({ range: diffEntry.modified.toInclusiveRange()!, options: modifiedDecoration }); @@ -275,9 +284,16 @@ export class ChatEditorController extends Disposable implements IEditorContribut stickiness: TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges } }); + + // Add line decorations for diff navigation + modifiedLineDecorations.push({ + range: diffEntry.modified.toInclusiveRange() ?? new Range(diffEntry.modified.startLineNumber, 1, diffEntry.modified.startLineNumber, Number.MAX_SAFE_INTEGER), + options: ChatEditorController._diffLineDecorationData + }); } - this._decorations.set(modifiedDecorations); + this._diffVisualDecorations.set(modifiedVisualDecorations); + this._diffLineDecorations.set(modifiedLineDecorations); }); const diffHunkDecoCollection = this._editor.createDecorationsCollection(diffHunkDecorations); @@ -352,6 +368,17 @@ export class ChatEditorController extends Disposable implements IEditorContribut })); } + initNavigation(): void { + const position = this._editor.getPosition(); + if (!position) { + return; + } + const range = this._diffLineDecorations.getRanges().find(r => r.containsPosition(position)); + if (range) { + this._currentChange.set(position, undefined); + } + } + revealNext(strict = false): boolean { return this._reveal(true, strict); } @@ -366,39 +393,24 @@ export class ChatEditorController extends Disposable implements IEditorContribut return false; } - const decorations: (Range | undefined)[] = this._decorations + const decorations = this._diffLineDecorations .getRanges() .sort((a, b) => Range.compareRangesUsingStarts(a, b)); - // TODO@jrieken this is slow and should be done smarter, e.g being able to read - // only whole range decorations because the goal is to go from change to change, skipping - // over word level changes - for (let i = 0; i < decorations.length; i++) { - const decoration = decorations[i]; - for (let j = 0; j < decorations.length; j++) { - if (i !== j && decoration && decorations[j]?.containsRange(decoration)) { - decorations[i] = undefined; - break; - } - } - } - - coalesceInPlace(decorations); - if (decorations.length === 0) { return false; } - let idx = binarySearch(decorations, Range.fromPositions(position), Range.compareRangesUsingStarts); - if (idx < 0) { - idx = ~idx; - } - - let target: number; - if (decorations[idx]?.containsPosition(position)) { - target = idx + (next ? 1 : -1); - } else { - target = next ? idx : idx - 1; + let target: number = -1; + for (let i = 0; i < decorations.length; i++) { + const range = decorations[i]; + if (range.containsPosition(position)) { + target = i + (next ? 1 : -1); + break; + } else if (Position.isBefore(position, range.getStartPosition())) { + target = next ? i : i - 1; + break; + } } if (strict && (target < 0 || target >= decorations.length)) { @@ -407,7 +419,10 @@ export class ChatEditorController extends Disposable implements IEditorContribut target = (target + decorations.length) % decorations.length; - const targetPosition = decorations[target].getStartPosition(); + const targetPosition = next ? decorations[target].getStartPosition() : decorations[target].getEndPosition(); + + this._currentChange.set(targetPosition, undefined); + this._editor.setPosition(targetPosition); this._editor.revealPositionInCenter(targetPosition, ScrollType.Smooth); this._editor.focus(); diff --git a/src/vs/workbench/contrib/chat/browser/chatEditorOverlay.ts b/src/vs/workbench/contrib/chat/browser/chatEditorOverlay.ts index 29bff0604ed90..23bf4638635db 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditorOverlay.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditorOverlay.ts @@ -27,6 +27,7 @@ import { localize } from '../../../../nls.js'; import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; import { ctxNotebookHasEditorModification } from '../../notebook/browser/contrib/chatEdit/notebookChatEditController.js'; import { AcceptAction, RejectAction } from './chatEditorActions.js'; +import { ChatEditorController } from './chatEditorController.js'; class ChatEditorOverlayWidget implements IOverlayWidget { @@ -206,16 +207,9 @@ class ChatEditorOverlayWidget implements IOverlayWidget { ); })); - - const editorPositionObs = observableFromEvent(this._editor.onDidChangeCursorPosition, () => this._editor.getPosition()); - this._showStore.add(autorun(r => { - const position = editorPositionObs.read(r); - - if (!position) { - return; - } + const position = ChatEditorController.get(this._editor)?.currentChange.read(r); const entries = session.entries.read(r); let changes = 0; @@ -229,7 +223,7 @@ class ChatEditorOverlayWidget implements IOverlayWidget { } else { for (const change of diffInfo.changes) { - if (change.modified.includes(position.lineNumber)) { + if (position && change.modified.includes(position.lineNumber)) { activeIdx = changes; } changes += 1; From 2f0eedb5eae0503eb89d9981376f4c3d03fe16d9 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Mon, 25 Nov 2024 15:10:30 +0100 Subject: [PATCH 101/119] fix #234547 (#234554) * fix #234547 * fix tests --- .../browser/extensionsWorkbenchService.ts | 29 ++++++++++++++----- .../extensionsActions.test.ts | 2 ++ 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts index c6ea38632404f..ab89df22ba9e8 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts @@ -787,6 +787,7 @@ class Extensions extends Disposable { } private async onDidInstallExtensions(results: readonly InstallExtensionResult[]): Promise { + const extensions: Extension[] = []; for (const event of results) { const { local, source } = event; const gallery = source && !URI.isUri(source) ? source : undefined; @@ -809,14 +810,19 @@ class Extensions extends Disposable { if (!extension.gallery) { extension.gallery = gallery; } - extension.setExtensionsControlManifest(await this.server.extensionManagementService.getExtensionsControlManifest()); extension.enablementState = this.extensionEnablementService.getEnablementState(local); } + extensions.push(extension); } this._onChange.fire(!local || !extension ? undefined : { extension, operation: event.operation }); - if (extension && extension.local && !extension.gallery && extension.local.source !== 'resource') { - await this.syncInstalledExtensionWithGallery(extension); + } + + if (extensions.length) { + const manifest = await this.server.extensionManagementService.getExtensionsControlManifest(); + for (const extension of extensions) { + extension.setExtensionsControlManifest(manifest); } + this.matchInstalledExtensionsWithGallery(extensions); } } @@ -832,7 +838,11 @@ class Extensions extends Disposable { } } - private async syncInstalledExtensionWithGallery(extension: Extension): Promise { + private async matchInstalledExtensionsWithGallery(extensions: Extension[]): Promise { + const toMatch = extensions.filter(e => e.local && !e.gallery && e.local.source !== 'resource'); + if (!toMatch.length) { + return; + } if (!this.galleryService.isEnabled()) { return; } @@ -841,10 +851,13 @@ class Extensions extends Disposable { comment: 'Report when a request is made to match installed extension with gallery'; }; this.telemetryService.publicLog2<{}, GalleryServiceMatchInstalledExtensionClassification>('galleryService:matchInstalledExtension'); - const [compatible] = await this.galleryService.getExtensions([{ ...extension.identifier, preRelease: extension.local?.preRelease }], { compatible: true, targetPlatform: await this.server.extensionManagementService.getTargetPlatform() }, CancellationToken.None); - if (compatible) { - extension.gallery = compatible; - this._onChange.fire({ extension }); + const galleryExtensions = await this.galleryService.getExtensions(toMatch.map(e => ({ ...e.identifier, preRelease: e.local?.preRelease })), { compatible: true, targetPlatform: await this.server.extensionManagementService.getTargetPlatform() }, CancellationToken.None); + for (const extension of extensions) { + const compatible = galleryExtensions.find(e => areSameExtensions(e.identifier, extension.identifier)); + if (compatible) { + extension.gallery = compatible; + this._onChange.fire({ extension }); + } } } diff --git a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsActions.test.ts b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsActions.test.ts index 37349a68d762c..dca3201e44d98 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsActions.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsActions.test.ts @@ -1030,6 +1030,7 @@ suite('ExtensionRuntimeStateAction', () => { canAddExtension: (extension) => true, whenInstalledExtensionsRegistered: () => Promise.resolve(true) }); + instantiationService.set(IExtensionsWorkbenchService, disposables.add(instantiationService.createInstance(ExtensionsWorkbenchService))); const testObject: ExtensionsActions.ExtensionRuntimeStateAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ExtensionRuntimeStateAction)); disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); const gallery = aGalleryExtension('a'); @@ -1118,6 +1119,7 @@ suite('ExtensionRuntimeStateAction', () => { canAddExtension: (extension) => false, whenInstalledExtensionsRegistered: () => Promise.resolve(true) }); + instantiationService.set(IExtensionsWorkbenchService, disposables.add(instantiationService.createInstance(ExtensionsWorkbenchService))); const testObject: ExtensionsActions.ExtensionRuntimeStateAction = disposables.add(instantiationService.createInstance(ExtensionsActions.ExtensionRuntimeStateAction)); disposables.add(instantiationService.createInstance(ExtensionContainers, [testObject])); const local = aLocalExtension('a'); From c3fda149e9b3490fed40bd1202fde82e9ca0c318 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 25 Nov 2024 06:39:44 -0800 Subject: [PATCH 102/119] Only support regular inline decorations on GPU for now Fixes #234570 --- src/vs/editor/browser/gpu/viewGpuContext.ts | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/src/vs/editor/browser/gpu/viewGpuContext.ts b/src/vs/editor/browser/gpu/viewGpuContext.ts index b7c2bb9cbe82d..54e4a4892bd87 100644 --- a/src/vs/editor/browser/gpu/viewGpuContext.ts +++ b/src/vs/editor/browser/gpu/viewGpuContext.ts @@ -22,6 +22,7 @@ import type { ViewContext } from '../../common/viewModel/viewContext.js'; import { DecorationCssRuleExtractor } from './decorationCssRuleExtractor.js'; import { Event } from '../../../base/common/event.js'; import type { IEditorOptions } from '../../common/config/editorOptions.js'; +import { InlineDecorationType } from '../../common/viewModel.js'; const enum GpuRenderLimits { maxGpuLines = 3000, @@ -158,6 +159,10 @@ export class ViewGpuContext extends Disposable { if (data.inlineDecorations.length > 0) { let supported = true; for (const decoration of data.inlineDecorations) { + if (decoration.type !== InlineDecorationType.Regular) { + supported = false; + break; + } const styleRules = ViewGpuContext._decorationCssRuleExtractor.getStyleRules(this.canvas.domNode, decoration.inlineClassName); supported &&= styleRules.every(rule => { // Pseudo classes aren't supported currently @@ -198,9 +203,15 @@ export class ViewGpuContext extends Disposable { } if (data.inlineDecorations.length > 0) { let supported = true; + const problemTypes: InlineDecorationType[] = []; const problemSelectors: string[] = []; const problemRules: string[] = []; for (const decoration of data.inlineDecorations) { + if (decoration.type !== InlineDecorationType.Regular) { + problemTypes.push(decoration.type); + supported = false; + continue; + } const styleRules = ViewGpuContext._decorationCssRuleExtractor.getStyleRules(this.canvas.domNode, decoration.inlineClassName); supported &&= styleRules.every(rule => { // Pseudo classes aren't supported currently @@ -217,14 +228,17 @@ export class ViewGpuContext extends Disposable { return true; }); if (!supported) { - break; + continue; } } + if (problemTypes.length > 0) { + reasons.push(`inlineDecorations with unsupported types (${problemTypes.map(e => `\`${e}\``).join(', ')})`); + } if (problemRules.length > 0) { - reasons.push(`inlineDecorations with unsupported CSS rules (\`${problemRules.join(', ')}\`)`); + reasons.push(`inlineDecorations with unsupported CSS rules (${problemRules.map(e => `\`${e}\``).join(', ')})`); } if (problemSelectors.length > 0) { - reasons.push(`inlineDecorations with unsupported CSS selectors (\`${problemSelectors.join(', ')}\`)`); + reasons.push(`inlineDecorations with unsupported CSS selectors (${problemSelectors.map(e => `\`${e}\``).join(', ')})`); } } if (lineNumber >= GpuRenderLimits.maxGpuLines) { From 417395e0e4bf79d96af00098913e9f0f85913455 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Mon, 25 Nov 2024 15:23:30 +0100 Subject: [PATCH 103/119] Support object type policy (#234428) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * #84756 Support object type policy * add new policy object for array and object * add new policy object type * checkin policies.js file * review * fix warning --------- Co-authored-by: João Moreno --- build/lib/policies.js | 25 +++++++++-- build/lib/policies.ts | 45 +++++++++++++++++-- .../configuration/common/configurations.ts | 24 +++++++--- .../test/common/policyConfiguration.test.ts | 34 ++++++++++++++ .../browser/extensions.contribution.ts | 7 ++- .../browser/extensionsWorkbenchService.ts | 7 ++- .../common/extensionManagement.ts | 8 ---- 7 files changed, 125 insertions(+), 25 deletions(-) diff --git a/build/lib/policies.js b/build/lib/policies.js index 466295b8ad543..aaa59a956579a 100644 --- a/build/lib/policies.js +++ b/build/lib/policies.js @@ -37,7 +37,7 @@ function renderADMLString(prefix, moduleName, nlsString, translations) { if (!value) { value = nlsString.value; } - return `${value}`; + return `${value}`; } class BasePolicy { policyType; @@ -59,7 +59,7 @@ class BasePolicy { } renderADMX(regKey) { return [ - ``, + ``, ` `, ` `, ` `, @@ -145,6 +145,24 @@ class StringPolicy extends BasePolicy { return ``; } } +class ObjectPolicy extends BasePolicy { + static from(name, category, minimumVersion, description, moduleName, settingNode) { + const type = getStringProperty(settingNode, 'type'); + if (type !== 'object' && type !== 'array') { + return undefined; + } + return new ObjectPolicy(name, category, minimumVersion, description, moduleName); + } + constructor(name, category, minimumVersion, description, moduleName) { + super(PolicyType.StringEnum, name, category, minimumVersion, description, moduleName); + } + renderADMXElements() { + return [``]; + } + renderADMLPresentationContents() { + return ``; + } +} class StringEnumPolicy extends BasePolicy { enum_; enumDescriptions; @@ -264,6 +282,7 @@ const PolicyTypes = [ IntPolicy, StringEnumPolicy, StringPolicy, + ObjectPolicy ]; function getPolicy(moduleName, configurationNode, settingNode, policyNode, categories) { const name = getStringProperty(policyNode, 'name'); @@ -319,7 +338,7 @@ function getPolicies(moduleName, node) { arguments: (arguments (object (pair key: [(property_identifier)(string)] @propertiesKey (#eq? @propertiesKey properties) value: (object (pair - key: [(property_identifier)(string)] + key: [(property_identifier)(string)(computed_property_name)] value: (object (pair key: [(property_identifier)(string)] @policyKey (#eq? @policyKey policy) value: (object) @policy diff --git a/build/lib/policies.ts b/build/lib/policies.ts index 68f6989f27a7d..c00739f289405 100644 --- a/build/lib/policies.ts +++ b/build/lib/policies.ts @@ -59,7 +59,7 @@ function renderADMLString(prefix: string, moduleName: string, nlsString: NlsStri value = nlsString.value; } - return `${value}`; + return `${value}`; } abstract class BasePolicy implements Policy { @@ -78,7 +78,7 @@ abstract class BasePolicy implements Policy { renderADMX(regKey: string) { return [ - ``, + ``, ` `, ` `, ` `, @@ -232,6 +232,44 @@ class StringPolicy extends BasePolicy { } } +class ObjectPolicy extends BasePolicy { + + static from( + name: string, + category: Category, + minimumVersion: string, + description: NlsString, + moduleName: string, + settingNode: Parser.SyntaxNode + ): ObjectPolicy | undefined { + const type = getStringProperty(settingNode, 'type'); + + if (type !== 'object' && type !== 'array') { + return undefined; + } + + return new ObjectPolicy(name, category, minimumVersion, description, moduleName); + } + + private constructor( + name: string, + category: Category, + minimumVersion: string, + description: NlsString, + moduleName: string, + ) { + super(PolicyType.StringEnum, name, category, minimumVersion, description, moduleName); + } + + protected renderADMXElements(): string[] { + return [``]; + } + + renderADMLPresentationContents() { + return ``; + } +} + class StringEnumPolicy extends BasePolicy { static from( @@ -402,6 +440,7 @@ const PolicyTypes = [ IntPolicy, StringEnumPolicy, StringPolicy, + ObjectPolicy ]; function getPolicy( @@ -474,7 +513,7 @@ function getPolicies(moduleName: string, node: Parser.SyntaxNode): Policy[] { arguments: (arguments (object (pair key: [(property_identifier)(string)] @propertiesKey (#eq? @propertiesKey properties) value: (object (pair - key: [(property_identifier)(string)] + key: [(property_identifier)(string)(computed_property_name)] value: (object (pair key: [(property_identifier)(string)] @policyKey (#eq? @policyKey policy) value: (object) @policy diff --git a/src/vs/platform/configuration/common/configurations.ts b/src/vs/platform/configuration/common/configurations.ts index a2f08f0a4c2e9..fcfbebf8545c1 100644 --- a/src/vs/platform/configuration/common/configurations.ts +++ b/src/vs/platform/configuration/common/configurations.ts @@ -8,12 +8,13 @@ import { IStringDictionary } from '../../../base/common/collections.js'; import { Emitter, Event } from '../../../base/common/event.js'; import { Disposable } from '../../../base/common/lifecycle.js'; import { equals } from '../../../base/common/objects.js'; -import { isEmptyObject } from '../../../base/common/types.js'; +import { isEmptyObject, isString } from '../../../base/common/types.js'; import { ConfigurationModel } from './configurationModels.js'; import { Extensions, IConfigurationRegistry, IRegisteredConfigurationPropertySchema } from './configurationRegistry.js'; import { ILogService, NullLogService } from '../../log/common/log.js'; -import { IPolicyService, PolicyDefinition, PolicyName, PolicyValue } from '../../policy/common/policy.js'; +import { IPolicyService, PolicyDefinition, PolicyName } from '../../policy/common/policy.js'; import { Registry } from '../../registry/common/platform.js'; +import { getErrorMessage } from '../../../base/common/errors.js'; export class DefaultConfiguration extends Disposable { @@ -122,12 +123,12 @@ export class PolicyConfiguration extends Disposable implements IPolicyConfigurat continue; } if (config.policy) { - if (config.type !== 'string' && config.type !== 'number') { + if (config.type !== 'string' && config.type !== 'number' && config.type !== 'array' && config.type !== 'object') { this.logService.warn(`Policy ${config.policy.name} has unsupported type ${config.type}`); continue; } keys.push(key); - policyDefinitions[config.policy.name] = { type: config.type }; + policyDefinitions[config.policy.name] = { type: config.type === 'number' ? 'number' : 'string' }; } } @@ -148,13 +149,22 @@ export class PolicyConfiguration extends Disposable implements IPolicyConfigurat private update(keys: string[], trigger: boolean): void { this.logService.trace('PolicyConfiguration#update', keys); const configurationProperties = Registry.as(Extensions.Configuration).getConfigurationProperties(); - const changed: [string, PolicyValue | undefined][] = []; + const changed: [string, any][] = []; const wasEmpty = this._configurationModel.isEmpty(); for (const key of keys) { - const policyName = configurationProperties[key]?.policy?.name; + const proprety = configurationProperties[key]; + const policyName = proprety?.policy?.name; if (policyName) { - const policyValue = this.policyService.getPolicyValue(policyName); + let policyValue = this.policyService.getPolicyValue(policyName); + if (isString(policyValue) && proprety.type !== 'string') { + try { + policyValue = JSON.parse(policyValue); + } catch (e) { + this.logService.error(`Error parsing policy value ${policyName}:`, getErrorMessage(e)); + continue; + } + } if (wasEmpty ? policyValue !== undefined : !equals(this._configurationModel.getValue(key), policyValue)) { changed.push([key, policyValue]); } diff --git a/src/vs/platform/configuration/test/common/policyConfiguration.test.ts b/src/vs/platform/configuration/test/common/policyConfiguration.test.ts index 44ef961ee9874..924ca6a8b50f3 100644 --- a/src/vs/platform/configuration/test/common/policyConfiguration.test.ts +++ b/src/vs/platform/configuration/test/common/policyConfiguration.test.ts @@ -50,6 +50,22 @@ suite('PolicyConfiguration', () => { minimumVersion: '1.0.0', } }, + 'policy.objectSetting': { + 'type': 'object', + 'default': {}, + policy: { + name: 'PolicyObjectSetting', + minimumVersion: '1.0.0', + } + }, + 'policy.arraySetting': { + 'type': 'object', + 'default': [], + policy: { + name: 'PolicyArraySetting', + minimumVersion: '1.0.0', + } + }, 'nonPolicy.setting': { 'type': 'boolean', 'default': true @@ -107,6 +123,24 @@ suite('PolicyConfiguration', () => { assert.deepStrictEqual(acutal.overrides, []); }); + test('initialize: with object type policy', async () => { + await fileService.writeFile(policyFile, VSBuffer.fromString(JSON.stringify({ 'PolicyObjectSetting': JSON.stringify({ 'a': 'b' }) }))); + + await testObject.initialize(); + const acutal = testObject.configurationModel; + + assert.deepStrictEqual(acutal.getValue('policy.objectSetting'), { 'a': 'b' }); + }); + + test('initialize: with array type policy', async () => { + await fileService.writeFile(policyFile, VSBuffer.fromString(JSON.stringify({ 'PolicyArraySetting': JSON.stringify([1]) }))); + + await testObject.initialize(); + const acutal = testObject.configurationModel; + + assert.deepStrictEqual(acutal.getValue('policy.arraySetting'), [1]); + }); + test('change: when policy is added', async () => { await fileService.writeFile(policyFile, VSBuffer.fromString(JSON.stringify({ 'PolicySettingA': 'policyValueA' }))); await testObject.initialize(); diff --git a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts index 27bfbd1d01093..575eebe161823 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts @@ -9,7 +9,7 @@ import { Registry } from '../../../../platform/registry/common/platform.js'; import { MenuRegistry, MenuId, registerAction2, Action2, IMenuItem, IAction2Options } from '../../../../platform/actions/common/actions.js'; import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; import { ExtensionsLocalizedLabel, IExtensionManagementService, IExtensionGalleryService, PreferencesLocalizedLabel, EXTENSION_INSTALL_SOURCE_CONTEXT, ExtensionInstallSource, UseUnpkgResourceApiConfigKey, AllowedExtensionsConfigKey } from '../../../../platform/extensionManagement/common/extensionManagement.js'; -import { EnablementState, IExtensionManagementServerService, IWorkbenchExtensionEnablementService, IWorkbenchExtensionManagementService, extensionsConfigurationNodeBase } from '../../../services/extensionManagement/common/extensionManagement.js'; +import { EnablementState, IExtensionManagementServerService, IWorkbenchExtensionEnablementService, IWorkbenchExtensionManagementService } from '../../../services/extensionManagement/common/extensionManagement.js'; import { IExtensionIgnoredRecommendationsService, IExtensionRecommendationsService } from '../../../services/extensionRecommendations/common/extensionRecommendations.js'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from '../../../common/contributions.js'; import { SyncDescriptor } from '../../../../platform/instantiation/common/descriptors.js'; @@ -122,7 +122,10 @@ Registry.as(ViewContainerExtensions.ViewContainersRegis Registry.as(ConfigurationExtensions.Configuration) .registerConfiguration({ - ...extensionsConfigurationNodeBase, + id: 'extensions', + order: 30, + title: localize('extensionsConfigurationTitle', "Extensions"), + type: 'object', properties: { 'extensions.autoUpdate': { enum: [true, 'onlyEnabledExtensions', false,], diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts index ab89df22ba9e8..3325a34ee12b7 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts @@ -21,7 +21,7 @@ import { TargetPlatformToString, IAllowedExtensionsService } from '../../../../platform/extensionManagement/common/extensionManagement.js'; -import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionManagementServer, IWorkbenchExtensionManagementService, DefaultIconPath, IResourceExtension, extensionsConfigurationNodeBase } from '../../../services/extensionManagement/common/extensionManagement.js'; +import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionManagementServer, IWorkbenchExtensionManagementService, DefaultIconPath, IResourceExtension } from '../../../services/extensionManagement/common/extensionManagement.js'; import { getGalleryExtensionTelemetryData, getLocalExtensionTelemetryData, areSameExtensions, groupByExtension, getGalleryExtensionId } from '../../../../platform/extensionManagement/common/extensionManagementUtil.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; @@ -1033,7 +1033,10 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension private registerAutoRestartConfig(): void { Registry.as(ConfigurationExtensions.Configuration) .registerConfiguration({ - ...extensionsConfigurationNodeBase, + id: 'extensions', + order: 30, + title: nls.localize('extensionsConfigurationTitle', "Extensions"), + type: 'object', properties: { [AutoRestartConfigurationKey]: { type: 'boolean', diff --git a/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts b/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts index 9028a14502044..c7baf279a3d36 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts @@ -9,7 +9,6 @@ import { IExtension, ExtensionType, IExtensionManifest, IExtensionIdentifier } f import { IExtensionManagementService, IGalleryExtension, ILocalExtension, InstallOptions, InstallExtensionEvent, DidUninstallExtensionEvent, InstallExtensionResult, Metadata, UninstallExtensionEvent, DidUpdateExtensionMetadata } from '../../../../platform/extensionManagement/common/extensionManagement.js'; import { URI } from '../../../../base/common/uri.js'; import { FileAccess } from '../../../../base/common/network.js'; -import { localize } from '../../../../nls.js'; import { IMarkdownString } from '../../../../base/common/htmlContent.js'; export type DidChangeProfileEvent = { readonly added: ILocalExtension[]; readonly removed: ILocalExtension[] }; @@ -89,13 +88,6 @@ export interface IWorkbenchExtensionManagementService extends IProfileAwareExten updateMetadata(local: ILocalExtension, metadata: Partial): Promise; } -export const extensionsConfigurationNodeBase = { - id: 'extensions', - order: 30, - title: localize('extensionsConfigurationTitle', "Extensions"), - type: 'object' -}; - export const enum EnablementState { DisabledByTrustRequirement, DisabledByExtensionKind, From fbffc9277119552a299d6e304fc916b39873f8ad Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 25 Nov 2024 15:38:30 +0100 Subject: [PATCH 104/119] Refactor update methods in ChatSetupContextKeys to return promises for better async handling (#234574) * Refactor update methods in ChatSetupContextKeys to return promises for better async handling * . --- .../chat/browser/actions/chatActions.ts | 11 ++-------- .../chat/browser/chatSetup.contribution.ts | 20 +++++++++---------- 2 files changed, 12 insertions(+), 19 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts index e32c8ad24b127..88837c32e8c8a 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts @@ -473,15 +473,8 @@ export function registerChatActions() { menu: [ { id: MenuId.ChatCommandCenter, - group: 'z_learn', - order: 1, - when: ChatContextKeys.Setup.installed - }, - { - id: MenuId.ChatCommandCenter, - group: 'a_first', - order: 2, - when: ChatContextKeys.Setup.installed.toNegated() + group: 'z_end', + order: 1 } ] }); diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts b/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts index 30b87176a773e..c402061b11f8d 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts @@ -667,10 +667,10 @@ class ChatSetupContextKeys { this.updateContext(); } - update(context: { chatInstalled: boolean }): void; - update(context: { triggered: boolean }): void; - update(context: { entitled: boolean; limited: boolean }): void; - update(context: { triggered?: boolean; chatInstalled?: boolean; entitled?: boolean; limited?: boolean }): void { + update(context: { chatInstalled: boolean }): Promise; + update(context: { triggered: boolean }): Promise; + update(context: { entitled: boolean; limited: boolean }): Promise; + update(context: { triggered?: boolean; chatInstalled?: boolean; entitled?: boolean; limited?: boolean }): Promise { if (typeof context.chatInstalled === 'boolean') { this.storageService.store(ChatSetupContextKeys.CHAT_EXTENSION_INSTALLED, context.chatInstalled, StorageScope.PROFILE, StorageTarget.MACHINE); if (context.chatInstalled) { @@ -694,7 +694,7 @@ class ChatSetupContextKeys { this.chatSetupLimited = context.limited; } - this.updateContext(); + return this.updateContext(); } private async updateContext(): Promise { @@ -755,7 +755,7 @@ class ChatSetupTriggerAction extends Action2 { const instantiationService = accessor.get(IInstantiationService); const configurationService = accessor.get(IConfigurationService); - instantiationService.createInstance(ChatSetupContextKeys).update({ triggered: true }); + await instantiationService.createInstance(ChatSetupContextKeys).update({ triggered: true }); showChatView(viewsService); @@ -766,7 +766,7 @@ class ChatSetupTriggerAction extends Action2 { class ChatSetupHideAction extends Action2 { static readonly ID = 'workbench.action.chat.hideSetup'; - static readonly TITLE = localize2('hideChatSetup', "Hide {0}...", defaultChat.name); + static readonly TITLE = localize2('hideChatSetup', "Hide {0}", defaultChat.name); constructor() { super({ @@ -776,8 +776,8 @@ class ChatSetupHideAction extends Action2 { precondition: ChatContextKeys.Setup.installed.negate(), menu: { id: MenuId.ChatCommandCenter, - group: 'z_hide', - order: 1, + group: 'z_end', + order: 2, when: ChatContextKeys.Setup.installed.negate() } }); @@ -802,7 +802,7 @@ class ChatSetupHideAction extends Action2 { const location = viewsDescriptorService.getViewLocationById(ChatViewId); - instantiationService.createInstance(ChatSetupContextKeys).update({ triggered: false }); + await instantiationService.createInstance(ChatSetupContextKeys).update({ triggered: false }); if (location === ViewContainerLocation.AuxiliaryBar) { const activeContainers = viewsDescriptorService.getViewContainersByLocation(location).filter(container => viewsDescriptorService.getViewContainerModel(container).activeViewDescriptors.length > 0); From d783eb46c8b2b2b6e39e30ea0c23a4e79564a331 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 25 Nov 2024 15:46:14 +0100 Subject: [PATCH 105/119] Remove condition for editing participant registration in chat command center (#234578) --- .../workbench/contrib/chat/browser/actions/chatClearActions.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatClearActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatClearActions.ts index 746d4d74e3dc0..e67ca53cb1691 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatClearActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatClearActions.ts @@ -322,7 +322,6 @@ export function registerNewChatActions() { order: 1 }, { id: MenuId.ChatCommandCenter, - when: ChatContextKeys.editingParticipantRegistered, group: 'a_open', order: 2 }, { From 92a6338931c7a4474813c50f30ac0e51e70ccb88 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Mon, 25 Nov 2024 16:30:27 +0100 Subject: [PATCH 106/119] polish allowed extensions feature (#234585) --- .../extensionManagement/common/allowedExtensionsService.ts | 2 +- .../extensionManagement/node/extensionManagementService.ts | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/vs/platform/extensionManagement/common/allowedExtensionsService.ts b/src/vs/platform/extensionManagement/common/allowedExtensionsService.ts index 452c07b3f355b..ce978e5b430e4 100644 --- a/src/vs/platform/extensionManagement/common/allowedExtensionsService.ts +++ b/src/vs/platform/extensionManagement/common/allowedExtensionsService.ts @@ -118,7 +118,7 @@ export class AllowedExtensionsService extends Disposable implements IAllowedExte } return false; })) { - return extensionReason; + return new MarkdownString(nls.localize('specific version of extension not allowed', "the version {0} of this extension is not in the [allowed list]({1})", version, settingsCommandLink)); } return true; } diff --git a/src/vs/platform/extensionManagement/node/extensionManagementService.ts b/src/vs/platform/extensionManagement/node/extensionManagementService.ts index e4bcb6c08668c..cf6050d7456e0 100644 --- a/src/vs/platform/extensionManagement/node/extensionManagementService.ts +++ b/src/vs/platform/extensionManagement/node/extensionManagementService.ts @@ -152,6 +152,11 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi throw new Error(nls.localize('incompatible', "Unable to install extension '{0}' as it is not compatible with VS Code '{1}'.", extensionId, this.productService.version)); } + const allowedToInstall = this.allowedExtensionsService.isAllowed({ id: extensionId, version: manifest.version }); + if (allowedToInstall !== true) { + throw new Error(nls.localize('notAllowed', "This extension cannot be installed because {0}", allowedToInstall.value)); + } + const results = await this.installExtensions([{ manifest, extension: location, options }]); const result = results.find(({ identifier }) => areSameExtensions(identifier, { id: extensionId })); if (result?.local) { From 1659f743fed1989ae855474fcd9694c986d3ff86 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Mon, 25 Nov 2024 16:58:03 +0100 Subject: [PATCH 107/119] SCM - update input action bar cancel icon (#234584) --- src/vs/workbench/contrib/scm/browser/scmViewPane.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index 0f397632c2e54..4ee1c3aa7bf32 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -1396,7 +1396,7 @@ class SCMInputWidgetToolbar extends WorkbenchToolBar { this._cancelAction = new MenuItemAction({ id: SCMInputWidgetCommandId.CancelAction, title: localize('scmInputCancelAction', "Cancel"), - icon: Codicon.debugStop, + icon: Codicon.stopCircle, }, undefined, undefined, undefined, undefined, contextKeyService, commandService); } From 00dc7aa671fb7b75ed2dc33bd5b9b85f1fe4d44b Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Mon, 25 Nov 2024 16:58:54 +0100 Subject: [PATCH 108/119] Git - fix git blame hover action titles (#234583) --- extensions/git/src/blame.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/extensions/git/src/blame.ts b/extensions/git/src/blame.ts index b21b0b1ab372f..4f046ae41b686 100644 --- a/extensions/git/src/blame.ts +++ b/extensions/git/src/blame.ts @@ -109,13 +109,13 @@ export class GitBlameController { markdownString.appendMarkdown(`${blameInformation.message}\n\n`); markdownString.appendMarkdown(`---\n\n`); - markdownString.appendMarkdown(`[$(eye) View Commit](command:git.blameStatusBarItem.viewCommit?${encodeURIComponent(JSON.stringify([documentUri, blameInformation.id]))})`); + markdownString.appendMarkdown(`[$(eye) View Commit](command:git.blameStatusBarItem.viewCommit?${encodeURIComponent(JSON.stringify([documentUri, blameInformation.id]))} "${l10n.t('View Commit')}")`); markdownString.appendMarkdown('  |  '); - markdownString.appendMarkdown(`[$(copy) ${blameInformation.id.substring(0, 8)}](command:git.blameStatusBarItem.copyContent?${encodeURIComponent(JSON.stringify(blameInformation.id))})`); + markdownString.appendMarkdown(`[$(copy) ${blameInformation.id.substring(0, 8)}](command:git.blameStatusBarItem.copyContent?${encodeURIComponent(JSON.stringify(blameInformation.id))} "${l10n.t('Copy Commit Hash')}")`); if (blameInformation.message) { markdownString.appendMarkdown('  '); - markdownString.appendMarkdown(`[$(copy) Message](command:git.blameStatusBarItem.copyContent?${encodeURIComponent(JSON.stringify(blameInformation.message))})`); + markdownString.appendMarkdown(`[$(copy) Message](command:git.blameStatusBarItem.copyContent?${encodeURIComponent(JSON.stringify(blameInformation.message))} "${l10n.t('Copy Commit Message')}")`); } return markdownString; From 0f104c89a69b2c395dcfddbeac31f42d31fc9507 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 25 Nov 2024 17:06:57 +0100 Subject: [PATCH 109/119] Add activity service integration for chat setup progress indication (#234590) --- .../chat/browser/chatSetup.contribution.ts | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts b/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts index c402061b11f8d..002f7502437bd 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts @@ -46,6 +46,8 @@ import { MarkdownString } from '../../../../base/common/htmlContent.js'; import { IProgressService, ProgressLocation } from '../../../../platform/progress/common/progress.js'; import { Barrier, timeout } from '../../../../base/common/async.js'; import { IChatAgentService } from '../common/chatAgents.js'; +import { IActivityService, ProgressBadge } from '../../../services/activity/common/activity.js'; +import { CHAT_SIDEBAR_PANEL_ID } from './chatViewPane.js'; const defaultChat = { extensionId: product.defaultChatAgent?.extensionId ?? '', @@ -381,7 +383,8 @@ class ChatSetupController extends Disposable { @IProductService private readonly productService: IProductService, @ILogService private readonly logService: ILogService, @IProgressService private readonly progressService: IProgressService, - @IChatAgentService private readonly chatAgentService: IChatAgentService + @IChatAgentService private readonly chatAgentService: IChatAgentService, + @IActivityService private readonly activityService: IActivityService ) { super(); @@ -402,11 +405,21 @@ class ChatSetupController extends Disposable { } async setup(enableTelemetry: boolean, enableDetection: boolean): Promise { - return this.progressService.withProgress({ - location: ProgressLocation.Window, - command: ChatSetupTriggerAction.ID, - title: localize('setupChatProgress', "Setting up {0}...", defaultChat.name), - }, () => this.doSetup(enableTelemetry, enableDetection)); + const title = localize('setupChatProgress', "Setting up {0}...", defaultChat.name); + const badge = this.activityService.showViewContainerActivity(CHAT_SIDEBAR_PANEL_ID, { + badge: new ProgressBadge(() => title), + priority: 100 + }); + + try { + await this.progressService.withProgress({ + location: ProgressLocation.Window, + command: ChatSetupTriggerAction.ID, + title, + }, () => this.doSetup(enableTelemetry, enableDetection)); + } finally { + badge.dispose(); + } } private async doSetup(enableTelemetry: boolean, enableDetection: boolean): Promise { From afe190195a58d20c989c90e77fb80352fad14092 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Mon, 25 Nov 2024 17:55:40 +0100 Subject: [PATCH 110/119] Git - add template support for editor decoration/status bar item (#234582) * Refactor property names * Add template support * Refactor template variable names * Manually fix the merge --- extensions/git/package.json | 18 ++++++++ extensions/git/package.nls.json | 6 ++- extensions/git/src/blame.ts | 81 +++++++++++++++++++++++---------- extensions/git/src/git.ts | 8 ++-- 4 files changed, 84 insertions(+), 29 deletions(-) diff --git a/extensions/git/package.json b/extensions/git/package.json index bc8243c2b343c..1851ef8703886 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -3198,6 +3198,15 @@ "experimental" ] }, + "git.blame.editorDecoration.template": { + "type": "string", + "default": "${subject}, ${authorName} (${authorDateAgo})", + "markdownDescription": "%config.blameEditorDecoration.template%", + "scope": "resource", + "tags": [ + "experimental" + ] + }, "git.blame.statusBarItem.enabled": { "type": "boolean", "default": false, @@ -3206,6 +3215,15 @@ "tags": [ "experimental" ] + }, + "git.blame.statusBarItem.template": { + "type": "string", + "default": "$(git-commit) ${authorName} (${authorDateAgo})", + "markdownDescription": "%config.blameStatusBarItem.template%", + "scope": "resource", + "tags": [ + "experimental" + ] } } }, diff --git a/extensions/git/package.nls.json b/extensions/git/package.nls.json index 389c90593467c..1f6775a2da58a 100644 --- a/extensions/git/package.nls.json +++ b/extensions/git/package.nls.json @@ -276,8 +276,10 @@ "config.publishBeforeContinueOn.never": "Never publish unpublished Git state when using Continue Working On from a Git repository", "config.publishBeforeContinueOn.prompt": "Prompt to publish unpublished Git state when using Continue Working On from a Git repository", "config.similarityThreshold": "Controls the threshold of the similarity index (the amount of additions/deletions compared to the file's size) for changes in a pair of added/deleted files to be considered a rename. **Note:** Requires Git version `2.18.0` or later.", - "config.blameEditorDecoration.enabled": "Controls whether to show git blame information in the editor using editor decorations.", - "config.blameStatusBarItem.enabled": "Controls whether to show git blame information in the status bar.", + "config.blameEditorDecoration.enabled": "Controls whether to show blame information in the editor using editor decorations.", + "config.blameEditorDecoration.template": "Template for the blame information editor decoration. Supported variables:\n\n* `hash`: Commit hash\n\n* `hashShort`: First 8 characters of the commit hash\n\n* `subject`: First line of the commit message\n\n* `authorName`: Author name\n\n* `authorEmail`: Author email\n\n* `authorDate`: Author date\n\n* `authorDateAgo`: Time difference between now and the author date\n\n", + "config.blameStatusBarItem.enabled": "Controls whether to show blame information in the status bar.", + "config.blameStatusBarItem.template": "Template for the blame information status bar item. Supported variables:\n\n* `hash`: Commit hash\n\n* `hashShort`: First 8 characters of the commit hash\n\n* `subject`: First line of the commit message\n\n* `authorName`: Author name\n\n* `authorEmail`: Author email\n\n* `authorDate`: Author date\n\n* `authorDateAgo`: Time difference between now and the author date\n\n", "submenu.explorer": "Git", "submenu.commit": "Commit", "submenu.commit.amend": "Amend", diff --git a/extensions/git/src/blame.ts b/extensions/git/src/blame.ts index 4f046ae41b686..a34634032dc45 100644 --- a/extensions/git/src/blame.ts +++ b/extensions/git/src/blame.ts @@ -52,6 +52,32 @@ function mapModifiedLineNumberToOriginalLineNumber(lineNumber: number, changes: return lineNumber; } +type BlameInformationTemplateTokens = { + readonly hash: string; + readonly hashShort: string; + readonly subject: string; + readonly authorName: string; + readonly authorEmail: string; + readonly authorDate: string; + readonly authorDateAgo: string; +}; + +function formatBlameInformation(template: string, blameInformation: BlameInformation): string { + const templateTokens = { + hash: blameInformation.hash, + hashShort: blameInformation.hash.substring(0, 8), + subject: blameInformation.subject ?? '', + authorName: blameInformation.authorName ?? '', + authorEmail: blameInformation.authorEmail ?? '', + authorDate: new Date(blameInformation.authorDate ?? new Date()).toLocaleString(), + authorDateAgo: fromNow(blameInformation.authorDate ?? new Date(), true, true) + } satisfies BlameInformationTemplateTokens; + + return template.replace(/\$\{(.+?)\}/g, (_, token) => { + return token in templateTokens ? templateTokens[token as keyof BlameInformationTemplateTokens] : `\${${token}}`; + }); +} + interface RepositoryBlameInformation { readonly commit: string; /* commit used for blame information */ readonly blameInformation: Map; @@ -98,24 +124,24 @@ export class GitBlameController { if (blameInformation.authorName) { markdownString.appendMarkdown(`$(account) **${blameInformation.authorName}**`); - if (blameInformation.date) { - const dateString = new Date(blameInformation.date).toLocaleString(undefined, { year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric' }); - markdownString.appendMarkdown(`, $(history) ${fromNow(blameInformation.date, true, true)} (${dateString})`); + if (blameInformation.authorDate) { + const dateString = new Date(blameInformation.authorDate).toLocaleString(undefined, { year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric' }); + markdownString.appendMarkdown(`, $(history) ${fromNow(blameInformation.authorDate, true, true)} (${dateString})`); } markdownString.appendMarkdown('\n\n'); } - markdownString.appendMarkdown(`${blameInformation.message}\n\n`); + markdownString.appendMarkdown(`${blameInformation.subject}\n\n`); markdownString.appendMarkdown(`---\n\n`); - markdownString.appendMarkdown(`[$(eye) View Commit](command:git.blameStatusBarItem.viewCommit?${encodeURIComponent(JSON.stringify([documentUri, blameInformation.id]))} "${l10n.t('View Commit')}")`); + markdownString.appendMarkdown(`[$(eye) View Commit](command:git.blameStatusBarItem.viewCommit?${encodeURIComponent(JSON.stringify([documentUri, blameInformation.hash]))} "${l10n.t('View Commit')}")`); markdownString.appendMarkdown('  |  '); - markdownString.appendMarkdown(`[$(copy) ${blameInformation.id.substring(0, 8)}](command:git.blameStatusBarItem.copyContent?${encodeURIComponent(JSON.stringify(blameInformation.id))} "${l10n.t('Copy Commit Hash')}")`); + markdownString.appendMarkdown(`[$(copy) ${blameInformation.hash.substring(0, 8)}](command:git.blameStatusBarItem.copyContent?${encodeURIComponent(JSON.stringify(blameInformation.hash))} "${l10n.t('Copy Commit Hash')}")`); - if (blameInformation.message) { + if (blameInformation.subject) { markdownString.appendMarkdown('  '); - markdownString.appendMarkdown(`[$(copy) Message](command:git.blameStatusBarItem.copyContent?${encodeURIComponent(JSON.stringify(blameInformation.message))} "${l10n.t('Copy Commit Message')}")`); + markdownString.appendMarkdown(`[$(copy) Subject](command:git.blameStatusBarItem.copyContent?${encodeURIComponent(JSON.stringify(blameInformation.subject))} "${l10n.t('Copy Commit Subject')}")`); } return markdownString; @@ -282,13 +308,13 @@ class GitBlameEditorDecoration { } private _onDidChangeConfiguration(e: ConfigurationChangeEvent): void { - if (!e.affectsConfiguration('git.blame.editorDecoration.enabled')) { + if (!e.affectsConfiguration('git.blame.editorDecoration.enabled') && + !e.affectsConfiguration('git.blame.editorDecoration.template')) { return; } - const enabled = this._isEnabled(); for (const textEditor of window.visibleTextEditors) { - if (enabled) { + if (this._getConfiguration().enabled) { this._updateDecorations(textEditor); } else { textEditor.setDecorations(this._decorationType, []); @@ -296,13 +322,17 @@ class GitBlameEditorDecoration { } } - private _isEnabled(): boolean { + private _getConfiguration(): { enabled: boolean; template: string } { const config = workspace.getConfiguration('git'); - return config.get('blame.editorDecoration.enabled', false); + const enabled = config.get('blame.editorDecoration.enabled', false); + const template = config.get('blame.editorDecoration.template', '${subject}, ${authorName} (${authorDateAgo})'); + + return { enabled, template }; } private _updateDecorations(textEditor: TextEditor): void { - if (!this._isEnabled()) { + const { enabled, template } = this._getConfiguration(); + if (!enabled) { return; } @@ -315,7 +345,7 @@ class GitBlameEditorDecoration { const decorations = blameInformation.map(blame => { const contentText = typeof blame.blameInformation === 'string' ? blame.blameInformation - : `${blame.blameInformation.message ?? ''}, ${blame.blameInformation.authorName ?? ''} (${fromNow(blame.blameInformation.date ?? Date.now(), true, true)})`; + : formatBlameInformation(template, blame.blameInformation); const hoverMessage = this._controller.getBlameInformationHover(textEditor.document.uri, blame.blameInformation); return this._createDecoration(blame.lineNumber, contentText, hoverMessage); @@ -357,11 +387,12 @@ class GitBlameStatusBarItem { } private _onDidChangeConfiguration(e: ConfigurationChangeEvent): void { - if (!e.affectsConfiguration('git.blame.statusBarItem.enabled')) { + if (!e.affectsConfiguration('git.blame.statusBarItem.enabled') && + !e.affectsConfiguration('git.blame.statusBarItem.template')) { return; } - if (this._isEnabled()) { + if (this._getConfiguration().enabled) { if (window.activeTextEditor) { this._updateStatusBarItem(window.activeTextEditor); } @@ -372,7 +403,7 @@ class GitBlameStatusBarItem { } private _onDidChangeActiveTextEditor(): void { - if (!this._isEnabled()) { + if (!this._getConfiguration().enabled) { return; } @@ -383,13 +414,17 @@ class GitBlameStatusBarItem { } } - private _isEnabled(): boolean { + private _getConfiguration(): { enabled: boolean; template: string } { const config = workspace.getConfiguration('git'); - return config.get('blame.statusBarItem.enabled', false); + const enabled = config.get('blame.statusBarItem.enabled', false); + const template = config.get('blame.statusBarItem.template', '${authorName} (${authorDateAgo})'); + + return { enabled, template }; } private _updateStatusBarItem(textEditor: TextEditor): void { - if (!this._isEnabled() || textEditor !== window.activeTextEditor) { + const { enabled, template } = this._getConfiguration(); + if (!enabled || textEditor !== window.activeTextEditor) { return; } @@ -410,12 +445,12 @@ class GitBlameStatusBarItem { this._statusBarItem.tooltip = this._controller.getBlameInformationHover(textEditor.document.uri, blameInformation[0].blameInformation); this._statusBarItem.command = undefined; } else { - this._statusBarItem.text = `$(git-commit) ${blameInformation[0].blameInformation.authorName ?? ''} (${fromNow(blameInformation[0].blameInformation.date ?? new Date(), true, true)})`; + this._statusBarItem.text = formatBlameInformation(template, blameInformation[0].blameInformation); this._statusBarItem.tooltip = this._controller.getBlameInformationHover(textEditor.document.uri, blameInformation[0].blameInformation); this._statusBarItem.command = { title: l10n.t('View Commit'), command: 'git.blameStatusBarItem.viewCommit', - arguments: [textEditor.document.uri, blameInformation[0].blameInformation.id] + arguments: [textEditor.document.uri, blameInformation[0].blameInformation.hash] } satisfies Command; } diff --git a/extensions/git/src/git.ts b/extensions/git/src/git.ts index 155a52128b0c8..de8b503ce5483 100644 --- a/extensions/git/src/git.ts +++ b/extensions/git/src/git.ts @@ -1055,11 +1055,11 @@ function parseGitChanges(repositoryRoot: string, raw: string): Change[] { } export interface BlameInformation { - readonly id: string; - readonly date?: number; - readonly message?: string; + readonly hash: string; + readonly subject?: string; readonly authorName?: string; readonly authorEmail?: string; + readonly authorDate?: number; readonly ranges: { readonly startLineNumber: number; readonly endLineNumber: number; @@ -1113,7 +1113,7 @@ function parseGitBlame(data: string): BlameInformation[] { blameInformation.set(commitHash, existingCommit); } else { blameInformation.set(commitHash, { - id: commitHash, authorName, authorEmail, date: authorTime, message, ranges: [{ startLineNumber, endLineNumber }] + hash: commitHash, authorName, authorEmail, authorDate: authorTime, subject: message, ranges: [{ startLineNumber, endLineNumber }] }); } From 19c22d483d42d7dfde37ef1503444c88de0040d7 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Mon, 25 Nov 2024 12:02:51 -0500 Subject: [PATCH 111/119] make test spec array, use it to run tests (#234593) --- .../src/terminalSuggestMain.test.ts | 104 +++++++++--------- 1 file changed, 53 insertions(+), 51 deletions(-) diff --git a/extensions/terminal-suggest/src/terminalSuggestMain.test.ts b/extensions/terminal-suggest/src/terminalSuggestMain.test.ts index dfc3cd58e2217..dd41c408f0618 100644 --- a/extensions/terminal-suggest/src/terminalSuggestMain.test.ts +++ b/extensions/terminal-suggest/src/terminalSuggestMain.test.ts @@ -7,63 +7,65 @@ import { deepStrictEqual, strictEqual } from 'assert'; import 'mocha'; import { availableSpecs, getCompletionItemsFromSpecs } from './terminalSuggestMain'; -suite('Terminal Suggest', () => { +const availableCommands = ['cd', 'code', 'code-insiders']; +const codeOptions = ['-', '--add', '--category', '--diff', '--disable-extension', '--disable-extensions', '--disable-gpu', '--enable-proposed-api', '--extensions-dir', '--goto', '--help', '--inspect-brk-extensions', '--inspect-extensions', '--install-extension', '--list-extensions', '--locale', '--log', '--max-memory', '--merge', '--new-window', '--pre-release', '--prof-startup', '--profile', '--reuse-window', '--show-versions', '--status', '--sync', '--telemetry', '--uninstall-extension', '--user-data-dir', '--verbose', '--version', '--wait', '-a', '-d', '-g', '-h', '-m', '-n', '-r', '-s', '-v', '-w']; - const availableCommands = ['cd', 'code', 'code-insiders']; - const codeOptions = ['-', '--add', '--category', '--diff', '--disable-extension', '--disable-extensions', '--disable-gpu', '--enable-proposed-api', '--extensions-dir', '--goto', '--help', '--inspect-brk-extensions', '--inspect-extensions', '--install-extension', '--list-extensions', '--locale', '--log', '--max-memory', '--merge', '--new-window', '--pre-release', '--prof-startup', '--profile', '--reuse-window', '--show-versions', '--status', '--sync', '--telemetry', '--uninstall-extension', '--user-data-dir', '--verbose', '--version', '--wait', '-a', '-d', '-g', '-h', '-m', '-n', '-r', '-s', '-v', '-w']; +const testSpecs: ITestSpec[] = [ + { input: '|', expectedCompletionLabels: availableCommands, resourcesRequested: 'neither' }, + { input: 'c|', expectedCompletionLabels: availableCommands, resourcesRequested: 'neither' }, + { input: 'ls && c|', expectedCompletionLabels: availableCommands, resourcesRequested: 'neither' }, + { input: 'cd |', expectedCompletionLabels: ['~', '-'], resourcesRequested: 'folders' }, + { input: 'code|', expectedCompletionLabels: ['code-insiders'], resourcesRequested: 'neither' }, + { input: 'code-insiders|', expectedCompletionLabels: [], resourcesRequested: 'neither' }, + { input: 'code |', expectedCompletionLabels: codeOptions, resourcesRequested: 'neither' }, + { input: 'code --locale |', expectedCompletionLabels: ['bg', 'de', 'en', 'es', 'fr', 'hu', 'it', 'ja', 'ko', 'pt-br', 'ru', 'tr', 'zh-CN', 'zh-TW'], resourcesRequested: 'neither' }, + { input: 'code --diff |', expectedCompletionLabels: [], resourcesRequested: 'files' }, + { input: 'code -di|', expectedCompletionLabels: codeOptions.filter(o => o.startsWith('di')), resourcesRequested: 'neither' }, + { input: 'code --diff ./file1 |', expectedCompletionLabels: [], resourcesRequested: 'files' }, + { input: 'code --merge |', expectedCompletionLabels: [], resourcesRequested: 'files' }, + { input: 'code --merge ./file1 ./file2 |', expectedCompletionLabels: [], resourcesRequested: 'files' }, + { input: 'code --merge ./file1 ./file2 ./base |', expectedCompletionLabels: [], resourcesRequested: 'files' }, + { input: 'code --goto |', expectedCompletionLabels: [], resourcesRequested: 'files' }, + { input: 'code --user-data-dir |', expectedCompletionLabels: [], resourcesRequested: 'folders' }, + { input: 'code --profile |', expectedCompletionLabels: [], resourcesRequested: 'neither' }, + { input: 'code --install-extension |', expectedCompletionLabels: [], resourcesRequested: 'neither' }, + { input: 'code --uninstall-extension |', expectedCompletionLabels: [], resourcesRequested: 'neither' }, + { input: 'code --log |', expectedCompletionLabels: ['critical', 'error', 'warn', 'info', 'debug', 'trace', 'off'], resourcesRequested: 'neither' }, + { input: 'code --sync |', expectedCompletionLabels: ['on', 'off'], resourcesRequested: 'neither' }, + { input: 'code --extensions-dir |', expectedCompletionLabels: [], resourcesRequested: 'folders' }, + { input: 'code --list-extensions |', expectedCompletionLabels: codeOptions, resourcesRequested: 'neither' }, + { input: 'code --show-versions |', expectedCompletionLabels: codeOptions, resourcesRequested: 'neither' }, + { input: 'code --category |', expectedCompletionLabels: ['azure', 'data science', 'debuggers', 'extension packs', 'education', 'formatters', 'keymaps', 'language packs', 'linters', 'machine learning', 'notebooks', 'programming languages', 'scm providers', 'snippets', 'testing', 'themes', 'visualization', 'other'], resourcesRequested: 'neither' }, + { input: 'code --category a|', expectedCompletionLabels: ['azure'], resourcesRequested: 'neither' }, + { input: 'code-insiders --list-extensions |', expectedCompletionLabels: codeOptions, resourcesRequested: 'neither' }, + { input: 'code-insiders --show-versions |', expectedCompletionLabels: codeOptions, resourcesRequested: 'neither' }, + { input: 'code-insiders --category |', expectedCompletionLabels: ['azure', 'data science', 'debuggers', 'extension packs', 'education', 'formatters', 'keymaps', 'language packs', 'linters', 'machine learning', 'notebooks', 'programming languages', 'scm providers', 'snippets', 'testing', 'themes', 'visualization', 'other'], resourcesRequested: 'neither' }, + { input: 'code-insiders --category a|', expectedCompletionLabels: ['azure'], resourcesRequested: 'neither' }, + { input: 'code-insiders --category azure |', expectedCompletionLabels: [], resourcesRequested: 'neither' }, + { input: 'code | --locale', expectedCompletionLabels: codeOptions, resourcesRequested: 'neither' }, + { input: 'code --locale | && ls', expectedCompletionLabels: ['bg', 'de', 'en', 'es', 'fr', 'hu', 'it', 'ja', 'ko', 'pt-br', 'ru', 'tr', 'zh-CN', 'zh-TW'], resourcesRequested: 'neither' }, + { input: 'code-insiders | --locale', expectedCompletionLabels: codeOptions, resourcesRequested: 'neither' }, + { input: 'code-insiders --locale | && ls', expectedCompletionLabels: ['bg', 'de', 'en', 'es', 'fr', 'hu', 'it', 'ja', 'ko', 'pt-br', 'ru', 'tr', 'zh-CN', 'zh-TW'], resourcesRequested: 'neither' } +]; - suite('Cursor at the end of the command line', () => { - createTestCase('|', availableCommands, 'neither', availableSpecs); - createTestCase('c|', availableCommands, 'neither', availableSpecs); - createTestCase('ls && c|', availableCommands, 'neither', availableSpecs); - createTestCase('cd |', ['~', '-'], 'folders', availableSpecs); - createTestCase('code|', ['code-insiders'], 'neither', availableSpecs); - createTestCase('code-insiders|', [], 'neither', availableSpecs); - createTestCase('code |', codeOptions, 'neither', availableSpecs); - createTestCase('code --locale |', ['bg', 'de', 'en', 'es', 'fr', 'hu', 'it', 'ja', 'ko', 'pt-br', 'ru', 'tr', 'zh-CN', 'zh-TW'], 'neither', availableSpecs); - createTestCase('code --diff |', [], 'files', availableSpecs); - createTestCase('code -di|', codeOptions.filter(o => o.startsWith('di')), 'neither', availableSpecs); - createTestCase('code --diff ./file1 |', [], 'files', availableSpecs); - createTestCase('code --merge |', [], 'files', availableSpecs); - createTestCase('code --merge ./file1 ./file2 |', [], 'files', availableSpecs); - createTestCase('code --merge ./file1 ./file2 ./base |', [], 'files', availableSpecs); - createTestCase('code --goto |', [], 'files', availableSpecs); - createTestCase('code --user-data-dir |', [], 'folders', availableSpecs); - createTestCase('code --profile |', [], 'neither', availableSpecs); - createTestCase('code --install-extension |', [], 'neither', availableSpecs); - createTestCase('code --uninstall-extension |', [], 'neither', availableSpecs); - createTestCase('code --log |', ['critical', 'error', 'warn', 'info', 'debug', 'trace', 'off'], 'neither', availableSpecs); - createTestCase('code --sync |', ['on', 'off'], 'neither', availableSpecs); - createTestCase('code --extensions-dir |', [], 'folders', availableSpecs); - createTestCase('code --list-extensions |', codeOptions, 'neither', availableSpecs); - createTestCase('code --show-versions |', codeOptions, 'neither', availableSpecs); - createTestCase('code --category |', ['azure', 'data science', 'debuggers', 'extension packs', 'education', 'formatters', 'keymaps', 'language packs', 'linters', 'machine learning', 'notebooks', 'programming languages', 'scm providers', 'snippets', 'testing', 'themes', 'visualization', 'other'], 'neither', availableSpecs); - createTestCase('code --category a|', ['azure'], 'neither', availableSpecs); - createTestCase('code-insiders --list-extensions |', codeOptions, 'neither', availableSpecs); - createTestCase('code-insiders --show-versions |', codeOptions, 'neither', availableSpecs); - createTestCase('code-insiders --category |', ['azure', 'data science', 'debuggers', 'extension packs', 'education', 'formatters', 'keymaps', 'language packs', 'linters', 'machine learning', 'notebooks', 'programming languages', 'scm providers', 'snippets', 'testing', 'themes', 'visualization', 'other'], 'neither', availableSpecs); - createTestCase('code-insiders --category a|', ['azure'], 'neither', availableSpecs); - createTestCase('code-insiders --category azure |', [], 'neither', availableSpecs); - }); - suite('Cursor not at the end of the line', () => { - createTestCase('code | --locale', codeOptions, 'neither', availableSpecs); - createTestCase('code --locale | && ls', ['bg', 'de', 'en', 'es', 'fr', 'hu', 'it', 'ja', 'ko', 'pt-br', 'ru', 'tr', 'zh-CN', 'zh-TW'], 'neither', availableSpecs); - createTestCase('code-insiders | --locale', codeOptions, 'neither', availableSpecs); - createTestCase('code-insiders --locale | && ls', ['bg', 'de', 'en', 'es', 'fr', 'hu', 'it', 'ja', 'ko', 'pt-br', 'ru', 'tr', 'zh-CN', 'zh-TW'], 'neither', availableSpecs); - }); +interface ITestSpec { + input: string; + expectedCompletionLabels: string[]; + resourcesRequested: 'files' | 'folders' | 'both' | 'neither'; +} - function createTestCase(commandLineWithCursor: string, expectedCompletionLabels: string[], resourcesRequested: 'files' | 'folders' | 'both' | 'neither', availableSpecs: Fig.Spec[]): void { - const commandLine = commandLineWithCursor.split('|')[0]; - const cursorPosition = commandLineWithCursor.indexOf('|'); - const prefix = commandLine.slice(0, cursorPosition).split(' ').pop() || ''; - const filesRequested = resourcesRequested === 'files' || resourcesRequested === 'both'; - const foldersRequested = resourcesRequested === 'folders' || resourcesRequested === 'both'; - test(commandLineWithCursor, function () { +suite('Terminal Suggest', () => { + for (const testSpec of testSpecs) { + test(testSpec.input, () => { + const commandLine = testSpec.input.split('|')[0]; + const cursorPosition = testSpec.input.indexOf('|'); + const prefix = commandLine.slice(0, cursorPosition).split(' ').pop() || ''; + const filesRequested = testSpec.resourcesRequested === 'files' || testSpec.resourcesRequested === 'both'; + const foldersRequested = testSpec.resourcesRequested === 'folders' || testSpec.resourcesRequested === 'both'; const result = getCompletionItemsFromSpecs(availableSpecs, { commandLine, cursorPosition }, availableCommands, prefix); - deepStrictEqual(result.items.map(i => i.label).sort(), expectedCompletionLabels.sort()); + deepStrictEqual(result.items.map(i => i.label).sort(), testSpec.expectedCompletionLabels.sort()); strictEqual(result.filesRequested, filesRequested); strictEqual(result.foldersRequested, foldersRequested); }); } }); - From 35cd0d020d11706591ef210faa70fca6543d5539 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Mon, 25 Nov 2024 18:57:47 +0100 Subject: [PATCH 112/119] hide squiggles when showing inline chat hint for a line (#234599) --- .../common/services/markerDecorations.ts | 3 ++ .../services/markerDecorationsService.ts | 38 ++++++++++++++++++- .../browser/inlineChatCurrentLine.ts | 8 +++- 3 files changed, 46 insertions(+), 3 deletions(-) diff --git a/src/vs/editor/common/services/markerDecorations.ts b/src/vs/editor/common/services/markerDecorations.ts index 8b315075c937d..66f1943b000ad 100644 --- a/src/vs/editor/common/services/markerDecorations.ts +++ b/src/vs/editor/common/services/markerDecorations.ts @@ -9,6 +9,7 @@ import { IMarker } from '../../../platform/markers/common/markers.js'; import { Event } from '../../../base/common/event.js'; import { Range } from '../core/range.js'; import { URI } from '../../../base/common/uri.js'; +import { IDisposable } from '../../../base/common/lifecycle.js'; export const IMarkerDecorationsService = createDecorator('markerDecorationsService'); @@ -20,4 +21,6 @@ export interface IMarkerDecorationsService { getMarker(uri: URI, decoration: IModelDecoration): IMarker | null; getLiveMarkers(uri: URI): [Range, IMarker][]; + + addMarkerSuppression(uri: URI, range: Range): IDisposable; } diff --git a/src/vs/editor/common/services/markerDecorationsService.ts b/src/vs/editor/common/services/markerDecorationsService.ts index 7cbd80e89f81a..c25a85a6915b4 100644 --- a/src/vs/editor/common/services/markerDecorationsService.ts +++ b/src/vs/editor/common/services/markerDecorationsService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { IMarkerService, IMarker, MarkerSeverity, MarkerTag } from '../../../platform/markers/common/markers.js'; -import { Disposable, toDisposable } from '../../../base/common/lifecycle.js'; +import { Disposable, IDisposable, toDisposable } from '../../../base/common/lifecycle.js'; import { URI } from '../../../base/common/uri.js'; import { IModelDeltaDecoration, ITextModel, IModelDecorationOptions, TrackedRangeStickiness, OverviewRulerLane, IModelDecoration, MinimapPosition, IModelDecorationMinimapOptions } from '../model.js'; import { ClassName } from '../model/intervalTree.js'; @@ -19,6 +19,7 @@ import { Emitter, Event } from '../../../base/common/event.js'; import { minimapInfo, minimapWarning, minimapError } from '../../../platform/theme/common/colorRegistry.js'; import { BidirectionalMap, ResourceMap } from '../../../base/common/map.js'; import { diffSets } from '../../../base/common/collections.js'; +import { Iterable } from '../../../base/common/iterator.js'; export class MarkerDecorationsService extends Disposable implements IMarkerDecorationsService { @@ -27,6 +28,8 @@ export class MarkerDecorationsService extends Disposable implements IMarkerDecor private readonly _onDidChangeMarker = this._register(new Emitter()); readonly onDidChangeMarker: Event = this._onDidChangeMarker.event; + private readonly _suppressedRanges = new ResourceMap>(); + private readonly _markerDecorations = new ResourceMap(); constructor( @@ -56,6 +59,28 @@ export class MarkerDecorationsService extends Disposable implements IMarkerDecor return markerDecorations ? markerDecorations.getMarkers() : []; } + addMarkerSuppression(uri: URI, range: Range): IDisposable { + + let suppressedRanges = this._suppressedRanges.get(uri); + if (!suppressedRanges) { + suppressedRanges = new Set(); + this._suppressedRanges.set(uri, suppressedRanges); + } + suppressedRanges.add(range); + this._handleMarkerChange([uri]); + + return toDisposable(() => { + const suppressedRanges = this._suppressedRanges.get(uri); + if (suppressedRanges) { + suppressedRanges.delete(range); + if (suppressedRanges.size === 0) { + this._suppressedRanges.delete(uri); + } + this._handleMarkerChange([uri]); + } + }); + } + private _handleMarkerChange(changedResources: readonly URI[]): void { changedResources.forEach((resource) => { const markerDecorations = this._markerDecorations.get(resource); @@ -88,7 +113,16 @@ export class MarkerDecorationsService extends Disposable implements IMarkerDecor private _updateDecorations(markerDecorations: MarkerDecorations): void { // Limit to the first 500 errors/warnings - const markers = this._markerService.read({ resource: markerDecorations.model.uri, take: 500 }); + let markers = this._markerService.read({ resource: markerDecorations.model.uri, take: 500 }); + + // filter markers from suppressed ranges + const suppressedRanges = this._suppressedRanges.get(markerDecorations.model.uri); + if (suppressedRanges) { + markers = markers.filter(marker => { + return !Iterable.some(suppressedRanges, candidate => Range.areIntersectingOrTouching(candidate, marker)); + }); + } + if (markerDecorations.update(markers)) { this._onDidChangeMarker.fire(markerDecorations.model); } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatCurrentLine.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatCurrentLine.ts index e1b70cbbc8334..8aa9b86969aed 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatCurrentLine.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatCurrentLine.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable } from '../../../../base/common/lifecycle.js'; +import { Disposable, MutableDisposable } from '../../../../base/common/lifecycle.js'; import { ICodeEditor, MouseTargetType } from '../../../../editor/browser/editorBrowser.js'; import { IEditorContribution } from '../../../../editor/common/editorCommon.js'; import { localize, localize2 } from '../../../../nls.js'; @@ -29,6 +29,7 @@ import { ICommandService } from '../../../../platform/commands/common/commands.j import { InlineCompletionsController } from '../../../../editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.js'; import { ChatAgentLocation, IChatAgentService } from '../../chat/common/chatAgents.js'; import { MarkdownString } from '../../../../base/common/htmlContent.js'; +import { IMarkerDecorationsService } from '../../../../editor/common/services/markerDecorations.js'; export const CTX_INLINE_CHAT_SHOWING_HINT = new RawContextKey('inlineChatShowingHint', false, localize('inlineChatShowingHint', "Whether inline chat shows a contextual hint")); @@ -157,6 +158,7 @@ export class InlineChatHintsController extends Disposable implements IEditorCont @ICommandService commandService: ICommandService, @IKeybindingService keybindingService: IKeybindingService, @IChatAgentService chatAgentService: IChatAgentService, + @IMarkerDecorationsService markerDecorationService: IMarkerDecorationsService ) { super(); this._editor = editor; @@ -182,6 +184,7 @@ export class InlineChatHintsController extends Disposable implements IEditorCont } })); + const markerSuppression = this._store.add(new MutableDisposable()); const decos = this._editor.createDecorationsCollection(); const modelObs = observableFromEvent(editor.onDidChangeModel, () => editor.getModel()); @@ -201,6 +204,7 @@ export class InlineChatHintsController extends Disposable implements IEditorCont if (!visible || !kb || !position || ghostState !== undefined || !model) { decos.clear(); + markerSuppression.clear(); return; } @@ -234,6 +238,8 @@ export class InlineChatHintsController extends Disposable implements IEditorCont } } }]); + + markerSuppression.value = markerDecorationService.addMarkerSuppression(model.uri, model.validateRange(new Range(position.lineNumber, 1, position.lineNumber, Number.MAX_SAFE_INTEGER))); })); } From 9fa0df932b5eed6acb6bb557cfe92fd4a0938f04 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 25 Nov 2024 11:40:18 -0800 Subject: [PATCH 113/119] Render lines to the alphabetic baseline Fixes #234581 --- src/vs/editor/browser/gpu/atlas/atlas.ts | 3 +++ .../gpu/atlas/textureAtlasShelfAllocator.ts | 5 ++++- .../gpu/atlas/textureAtlasSlabAllocator.ts | 5 ++++- .../browser/gpu/fullFileRenderStrategy.ts | 18 +++++++++------ .../browser/gpu/raster/glyphRasterizer.ts | 22 +++++++++++++++---- src/vs/editor/browser/gpu/raster/raster.ts | 6 +++++ .../browser/gpu/atlas/textureAtlas.test.ts | 3 +++ .../gpu/atlas/textureAtlasAllocator.test.ts | 3 +++ 8 files changed, 52 insertions(+), 13 deletions(-) diff --git a/src/vs/editor/browser/gpu/atlas/atlas.ts b/src/vs/editor/browser/gpu/atlas/atlas.ts index a8a2fcee9aac1..3a05ab74b27a6 100644 --- a/src/vs/editor/browser/gpu/atlas/atlas.ts +++ b/src/vs/editor/browser/gpu/atlas/atlas.ts @@ -31,6 +31,9 @@ export interface ITextureAtlasPageGlyph { originOffsetX: number; /** The y offset from {@link y} of the glyph's origin. */ originOffsetY: number; + alphabeticBaseline: number; + fontBoundingBoxAscent: number; + fontBoundingBoxDescent: number; } /** diff --git a/src/vs/editor/browser/gpu/atlas/textureAtlasShelfAllocator.ts b/src/vs/editor/browser/gpu/atlas/textureAtlasShelfAllocator.ts index 1bbf920997f63..ea682d48929ca 100644 --- a/src/vs/editor/browser/gpu/atlas/textureAtlasShelfAllocator.ts +++ b/src/vs/editor/browser/gpu/atlas/textureAtlasShelfAllocator.ts @@ -80,7 +80,10 @@ export class TextureAtlasShelfAllocator implements ITextureAtlasAllocator { w: glyphWidth, h: glyphHeight, originOffsetX: rasterizedGlyph.originOffset.x, - originOffsetY: rasterizedGlyph.originOffset.y + originOffsetY: rasterizedGlyph.originOffset.y, + alphabeticBaseline: rasterizedGlyph.alphabeticBaseline, + fontBoundingBoxAscent: rasterizedGlyph.fontBoundingBoxAscent, + fontBoundingBoxDescent: rasterizedGlyph.fontBoundingBoxDescent, }; // Shift current row diff --git a/src/vs/editor/browser/gpu/atlas/textureAtlasSlabAllocator.ts b/src/vs/editor/browser/gpu/atlas/textureAtlasSlabAllocator.ts index 461fd507dbe15..2ad77ecc1dd1c 100644 --- a/src/vs/editor/browser/gpu/atlas/textureAtlasSlabAllocator.ts +++ b/src/vs/editor/browser/gpu/atlas/textureAtlasSlabAllocator.ts @@ -278,7 +278,10 @@ export class TextureAtlasSlabAllocator implements ITextureAtlasAllocator { w: glyphWidth, h: glyphHeight, originOffsetX: rasterizedGlyph.originOffset.x, - originOffsetY: rasterizedGlyph.originOffset.y + originOffsetY: rasterizedGlyph.originOffset.y, + alphabeticBaseline: rasterizedGlyph.alphabeticBaseline, + fontBoundingBoxAscent: rasterizedGlyph.fontBoundingBoxAscent, + fontBoundingBoxDescent: rasterizedGlyph.fontBoundingBoxDescent, }; // Set the glyph diff --git a/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts b/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts index 7ebc66d68e006..591d4708096cb 100644 --- a/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts +++ b/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts @@ -409,13 +409,17 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend // TODO: Support non-standard character widths absoluteOffsetX = Math.round((x + xOffset) * viewLineOptions.spaceWidth * dpr); - absoluteOffsetY = ( - Math.ceil(( - // Top of line including line height - viewportData.relativeVerticalOffset[y - viewportData.startLineNumber] + - // Delta to top of line after line height - Math.floor((viewportData.lineHeight - this._context.configuration.options.get(EditorOption.fontSize)) / 2) - ) * dpr) + absoluteOffsetY = Math.round( + // Top of layout box (includes line height) + viewportData.relativeVerticalOffset[y - viewportData.startLineNumber] * dpr + + + // Delta from top of layout box (includes line height) to top of the inline box (no line height) + Math.floor((viewportData.lineHeight * dpr - (glyph.fontBoundingBoxAscent + glyph.fontBoundingBoxDescent)) / 2) + + + // Delta from top of inline box (no line height) to top of glyph origin. If the glyph was drawn + // with a top baseline for example, this ends up drawing the glyph correctly using the alphabetical + // baseline. + glyph.fontBoundingBoxAscent ); cellIndex = ((y - 1) * this._viewGpuContext.maxGpuCols + x) * Constants.IndicesPerCell; diff --git a/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts b/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts index 5df78bc465e49..3e52ea6973abe 100644 --- a/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts +++ b/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts @@ -24,6 +24,8 @@ export class GlyphRasterizer extends Disposable implements IGlyphRasterizer { private _canvas: OffscreenCanvas; private _ctx: OffscreenCanvasRenderingContext2D; + private readonly _textMetrics: TextMetrics; + private _workGlyph: IRasterizedGlyph = { source: null!, boundingBox: { @@ -35,7 +37,10 @@ export class GlyphRasterizer extends Disposable implements IGlyphRasterizer { originOffset: { x: 0, y: 0, - } + }, + alphabeticBaseline: 0, + fontBoundingBoxAscent: 0, + fontBoundingBoxDescent: 0, }; private _workGlyphConfig: { chars: string | undefined; tokenMetadata: number; charMetadata: number } = { chars: undefined, tokenMetadata: 0, charMetadata: 0 }; @@ -52,6 +57,8 @@ export class GlyphRasterizer extends Disposable implements IGlyphRasterizer { })); this._ctx.textBaseline = 'top'; this._ctx.fillStyle = '#FFFFFF'; + this._ctx.font = `${devicePixelFontSize}px ${this.fontFamily}`; + this._textMetrics = this._ctx.measureText('A'); } // TODO: Support drawing multiple fonts and sizes @@ -69,7 +76,10 @@ export class GlyphRasterizer extends Disposable implements IGlyphRasterizer { return { source: this._canvas, boundingBox: { top: 0, left: 0, bottom: -1, right: -1 }, - originOffset: { x: 0, y: 0 } + originOffset: { x: 0, y: 0 }, + alphabeticBaseline: 0, + fontBoundingBoxAscent: 0, + fontBoundingBoxDescent: 0, }; } // Check if the last glyph matches the config, reuse if so. This helps avoid unnecessary @@ -122,9 +132,8 @@ export class GlyphRasterizer extends Disposable implements IGlyphRasterizer { } else { this._ctx.fillStyle = colorMap[TokenMetadata.getForeground(metadata)]; } - // TODO: This might actually be slower - // const textMetrics = this._ctx.measureText(chars); this._ctx.textBaseline = 'top'; + this._ctx.fillText(chars, originX, originY); const imageData = this._ctx.getImageData(0, 0, this._canvas.width, this._canvas.height); @@ -143,6 +152,11 @@ export class GlyphRasterizer extends Disposable implements IGlyphRasterizer { this._workGlyph.source = this._canvas; this._workGlyph.originOffset.x = this._workGlyph.boundingBox.left - originX; this._workGlyph.originOffset.y = this._workGlyph.boundingBox.top - originY; + // TODO: Clean this up, reduce duplication + this._workGlyph.alphabeticBaseline = this._textMetrics.alphabeticBaseline; + this._workGlyph.fontBoundingBoxAscent = this._textMetrics.fontBoundingBoxAscent; + this._workGlyph.fontBoundingBoxDescent = this._textMetrics.fontBoundingBoxDescent; + // const result2: IRasterizedGlyph = { // source: this._canvas, // boundingBox: { diff --git a/src/vs/editor/browser/gpu/raster/raster.ts b/src/vs/editor/browser/gpu/raster/raster.ts index c86b1649e34b7..1cc159addc034 100644 --- a/src/vs/editor/browser/gpu/raster/raster.ts +++ b/src/vs/editor/browser/gpu/raster/raster.ts @@ -64,4 +64,10 @@ export interface IRasterizedGlyph { * The offset to the glyph's origin (where it should be drawn to). */ originOffset: { x: number; y: number }; + /** + * The glyph's distance from the alphabetical baseline. + */ + alphabeticBaseline: number; + fontBoundingBoxAscent: number; + fontBoundingBoxDescent: number; } diff --git a/src/vs/editor/test/browser/gpu/atlas/textureAtlas.test.ts b/src/vs/editor/test/browser/gpu/atlas/textureAtlas.test.ts index 0610f84eb3e5e..e7a26a2d3c265 100644 --- a/src/vs/editor/test/browser/gpu/atlas/textureAtlas.test.ts +++ b/src/vs/editor/test/browser/gpu/atlas/textureAtlas.test.ts @@ -56,6 +56,9 @@ class TestGlyphRasterizer implements IGlyphRasterizer { source: canvas, boundingBox: { top: 0, left: 0, bottom: h - 1, right: w - 1 }, originOffset: { x: 0, y: 0 }, + alphabeticBaseline: 0, + fontBoundingBoxAscent: 0, + fontBoundingBoxDescent: 0, }; } } diff --git a/src/vs/editor/test/browser/gpu/atlas/textureAtlasAllocator.test.ts b/src/vs/editor/test/browser/gpu/atlas/textureAtlasAllocator.test.ts index 377bb752df8a0..a0276707009f2 100644 --- a/src/vs/editor/test/browser/gpu/atlas/textureAtlasAllocator.test.ts +++ b/src/vs/editor/test/browser/gpu/atlas/textureAtlasAllocator.test.ts @@ -29,6 +29,9 @@ function createRasterizedGlyph(w: number, h: number, data: ArrayLike): I source, boundingBox: { top: 0, left: 0, bottom: h - 1, right: w - 1 }, originOffset: { x: 0, y: 0 }, + alphabeticBaseline: 0, + fontBoundingBoxAscent: 0, + fontBoundingBoxDescent: 0, }; } From eab8277a368ae52edeacc43a53da1896a2b5e42e Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 25 Nov 2024 11:45:52 -0800 Subject: [PATCH 114/119] Remove alphabeticBaseline and add docs --- src/vs/editor/browser/gpu/atlas/atlas.ts | 13 ++++++++++++- .../browser/gpu/atlas/textureAtlasShelfAllocator.ts | 1 - .../browser/gpu/atlas/textureAtlasSlabAllocator.ts | 1 - src/vs/editor/browser/gpu/raster/glyphRasterizer.ts | 4 ---- src/vs/editor/browser/gpu/raster/raster.ts | 12 ++++++++++-- .../test/browser/gpu/atlas/textureAtlas.test.ts | 1 - .../browser/gpu/atlas/textureAtlasAllocator.test.ts | 1 - 7 files changed, 22 insertions(+), 11 deletions(-) diff --git a/src/vs/editor/browser/gpu/atlas/atlas.ts b/src/vs/editor/browser/gpu/atlas/atlas.ts index 3a05ab74b27a6..f4d87f2ae2ae2 100644 --- a/src/vs/editor/browser/gpu/atlas/atlas.ts +++ b/src/vs/editor/browser/gpu/atlas/atlas.ts @@ -31,8 +31,19 @@ export interface ITextureAtlasPageGlyph { originOffsetX: number; /** The y offset from {@link y} of the glyph's origin. */ originOffsetY: number; - alphabeticBaseline: number; + /** + * The distance from the the glyph baseline to the top of the highest bounding rectangle of all + * fonts used to render the text. + * + * @see {@link TextMetrics.fontBoundingBoxAscent} + */ fontBoundingBoxAscent: number; + /** + * The distance from the the glyph baseline to the bottom of the bounding rectangle of all fonts + * used to render the text. + * + * @see {@link TextMetrics.fontBoundingBoxDescent} + */ fontBoundingBoxDescent: number; } diff --git a/src/vs/editor/browser/gpu/atlas/textureAtlasShelfAllocator.ts b/src/vs/editor/browser/gpu/atlas/textureAtlasShelfAllocator.ts index ea682d48929ca..c22491a5f1cf5 100644 --- a/src/vs/editor/browser/gpu/atlas/textureAtlasShelfAllocator.ts +++ b/src/vs/editor/browser/gpu/atlas/textureAtlasShelfAllocator.ts @@ -81,7 +81,6 @@ export class TextureAtlasShelfAllocator implements ITextureAtlasAllocator { h: glyphHeight, originOffsetX: rasterizedGlyph.originOffset.x, originOffsetY: rasterizedGlyph.originOffset.y, - alphabeticBaseline: rasterizedGlyph.alphabeticBaseline, fontBoundingBoxAscent: rasterizedGlyph.fontBoundingBoxAscent, fontBoundingBoxDescent: rasterizedGlyph.fontBoundingBoxDescent, }; diff --git a/src/vs/editor/browser/gpu/atlas/textureAtlasSlabAllocator.ts b/src/vs/editor/browser/gpu/atlas/textureAtlasSlabAllocator.ts index 2ad77ecc1dd1c..f9fa20dcfdb2d 100644 --- a/src/vs/editor/browser/gpu/atlas/textureAtlasSlabAllocator.ts +++ b/src/vs/editor/browser/gpu/atlas/textureAtlasSlabAllocator.ts @@ -279,7 +279,6 @@ export class TextureAtlasSlabAllocator implements ITextureAtlasAllocator { h: glyphHeight, originOffsetX: rasterizedGlyph.originOffset.x, originOffsetY: rasterizedGlyph.originOffset.y, - alphabeticBaseline: rasterizedGlyph.alphabeticBaseline, fontBoundingBoxAscent: rasterizedGlyph.fontBoundingBoxAscent, fontBoundingBoxDescent: rasterizedGlyph.fontBoundingBoxDescent, }; diff --git a/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts b/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts index 3e52ea6973abe..6077f52a259e8 100644 --- a/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts +++ b/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts @@ -38,7 +38,6 @@ export class GlyphRasterizer extends Disposable implements IGlyphRasterizer { x: 0, y: 0, }, - alphabeticBaseline: 0, fontBoundingBoxAscent: 0, fontBoundingBoxDescent: 0, }; @@ -77,7 +76,6 @@ export class GlyphRasterizer extends Disposable implements IGlyphRasterizer { source: this._canvas, boundingBox: { top: 0, left: 0, bottom: -1, right: -1 }, originOffset: { x: 0, y: 0 }, - alphabeticBaseline: 0, fontBoundingBoxAscent: 0, fontBoundingBoxDescent: 0, }; @@ -152,8 +150,6 @@ export class GlyphRasterizer extends Disposable implements IGlyphRasterizer { this._workGlyph.source = this._canvas; this._workGlyph.originOffset.x = this._workGlyph.boundingBox.left - originX; this._workGlyph.originOffset.y = this._workGlyph.boundingBox.top - originY; - // TODO: Clean this up, reduce duplication - this._workGlyph.alphabeticBaseline = this._textMetrics.alphabeticBaseline; this._workGlyph.fontBoundingBoxAscent = this._textMetrics.fontBoundingBoxAscent; this._workGlyph.fontBoundingBoxDescent = this._textMetrics.fontBoundingBoxDescent; diff --git a/src/vs/editor/browser/gpu/raster/raster.ts b/src/vs/editor/browser/gpu/raster/raster.ts index 1cc159addc034..989a4656415c3 100644 --- a/src/vs/editor/browser/gpu/raster/raster.ts +++ b/src/vs/editor/browser/gpu/raster/raster.ts @@ -65,9 +65,17 @@ export interface IRasterizedGlyph { */ originOffset: { x: number; y: number }; /** - * The glyph's distance from the alphabetical baseline. + * The distance from the the glyph baseline to the top of the highest bounding rectangle of all + * fonts used to render the text. + * + * @see {@link TextMetrics.fontBoundingBoxAscent} */ - alphabeticBaseline: number; fontBoundingBoxAscent: number; + /** + * The distance from the the glyph baseline to the bottom of the bounding rectangle of all fonts + * used to render the text. + * + * @see {@link TextMetrics.fontBoundingBoxDescent} + */ fontBoundingBoxDescent: number; } diff --git a/src/vs/editor/test/browser/gpu/atlas/textureAtlas.test.ts b/src/vs/editor/test/browser/gpu/atlas/textureAtlas.test.ts index e7a26a2d3c265..5b2c44367c05b 100644 --- a/src/vs/editor/test/browser/gpu/atlas/textureAtlas.test.ts +++ b/src/vs/editor/test/browser/gpu/atlas/textureAtlas.test.ts @@ -56,7 +56,6 @@ class TestGlyphRasterizer implements IGlyphRasterizer { source: canvas, boundingBox: { top: 0, left: 0, bottom: h - 1, right: w - 1 }, originOffset: { x: 0, y: 0 }, - alphabeticBaseline: 0, fontBoundingBoxAscent: 0, fontBoundingBoxDescent: 0, }; diff --git a/src/vs/editor/test/browser/gpu/atlas/textureAtlasAllocator.test.ts b/src/vs/editor/test/browser/gpu/atlas/textureAtlasAllocator.test.ts index a0276707009f2..b8b6cabe9ebac 100644 --- a/src/vs/editor/test/browser/gpu/atlas/textureAtlasAllocator.test.ts +++ b/src/vs/editor/test/browser/gpu/atlas/textureAtlasAllocator.test.ts @@ -29,7 +29,6 @@ function createRasterizedGlyph(w: number, h: number, data: ArrayLike): I source, boundingBox: { top: 0, left: 0, bottom: h - 1, right: w - 1 }, originOffset: { x: 0, y: 0 }, - alphabeticBaseline: 0, fontBoundingBoxAscent: 0, fontBoundingBoxDescent: 0, }; From ffc6ea3dec3ed6ef7bc96f41a18c262cd9462540 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Mon, 25 Nov 2024 21:28:32 +0100 Subject: [PATCH 115/119] Git - refactor blame cache in preparation to more changes (#234604) --- extensions/git/src/blame.ts | 72 +++++++++++++++++-------------------- 1 file changed, 33 insertions(+), 39 deletions(-) diff --git a/extensions/git/src/blame.ts b/extensions/git/src/blame.ts index a34634032dc45..13c87fe50a41b 100644 --- a/extensions/git/src/blame.ts +++ b/extensions/git/src/blame.ts @@ -5,10 +5,11 @@ import { DecorationOptions, l10n, Position, Range, TextEditor, TextEditorChange, TextEditorDecorationType, TextEditorChangeKind, ThemeColor, Uri, window, workspace, EventEmitter, ConfigurationChangeEvent, StatusBarItem, StatusBarAlignment, Command, MarkdownString } from 'vscode'; import { Model } from './model'; -import { dispose, fromNow, IDisposable } from './util'; +import { dispose, fromNow, IDisposable, pathEquals } from './util'; import { Repository } from './repository'; import { throttle } from './decorators'; import { BlameInformation } from './git'; +import { fromGitUri, isGitUri } from './uri'; function lineRangesContainLine(changes: readonly TextEditorChange[], lineNumber: number): boolean { return changes.some(c => c.modified.startLineNumber <= lineNumber && lineNumber < c.modified.endLineNumberExclusive); @@ -79,8 +80,9 @@ function formatBlameInformation(template: string, blameInformation: BlameInforma } interface RepositoryBlameInformation { - readonly commit: string; /* commit used for blame information */ - readonly blameInformation: Map; + readonly commit: string; + readonly resource: Uri; + readonly blameInformation: BlameInformation[]; } interface LineBlameInformation { @@ -94,7 +96,7 @@ export class GitBlameController { readonly textEditorBlameInformation = new Map(); - private readonly _repositoryBlameInformation = new Map(); + private readonly _repositoryBlameInformation = new Map(); private _repositoryDisposables = new Map(); private _disposables: IDisposable[] = []; @@ -170,41 +172,34 @@ export class GitBlameController { return; } - // HEAD commit changed (remove blame information for the repository) - if (repositoryBlameInformation.commit !== repository.HEAD?.commit) { - this._repositoryBlameInformation.delete(repository); - - for (const textEditor of window.visibleTextEditors) { - this._updateTextEditorBlameInformation(textEditor); - } + for (const textEditor of window.visibleTextEditors) { + this._updateTextEditorBlameInformation(textEditor); } } - private async _getBlameInformation(resource: Uri): Promise { + private async _getBlameInformation(resource: Uri, commit: string): Promise { const repository = this._model.getRepository(resource); - if (!repository || !repository.HEAD?.commit) { + if (!repository) { return undefined; } - const repositoryBlameInformation = this._repositoryBlameInformation.get(repository) ?? { - commit: repository.HEAD.commit, - blameInformation: new Map() - } satisfies RepositoryBlameInformation; - - let resourceBlameInformation = repositoryBlameInformation.blameInformation.get(resource); - if (repositoryBlameInformation.commit === repository.HEAD.commit && resourceBlameInformation) { - return resourceBlameInformation; + const repositoryBlameInformation = this._repositoryBlameInformation.get(repository) ?? []; + const resourceBlameInformation = repositoryBlameInformation + .find(b => pathEquals(b.resource.fsPath, resource.fsPath) && b.commit === commit); + if (resourceBlameInformation) { + return resourceBlameInformation.blameInformation; } - // Get blame information for the resource - resourceBlameInformation = await repository.blame2(resource.fsPath, repository.HEAD.commit) ?? []; + // Get blame information for the resource and cache it + const blameInformation = await repository.blame2(resource.fsPath, commit) ?? []; - this._repositoryBlameInformation.set(repository, { - ...repositoryBlameInformation, - blameInformation: repositoryBlameInformation.blameInformation.set(resource, resourceBlameInformation) - }); + repositoryBlameInformation.push({ + commit, resource, blameInformation + } satisfies RepositoryBlameInformation); + + this._repositoryBlameInformation.set(repository, repositoryBlameInformation); - return resourceBlameInformation; + return blameInformation; } @throttle @@ -213,21 +208,20 @@ export class GitBlameController { return; } + const repository = this._model.getRepository(textEditor.document.uri); + if (!repository || !repository.HEAD?.commit) { + return; + } + // Working tree diff information const diffInformationWorkingTree = textEditor.diffInformation - .filter(diff => diff.original?.scheme === 'git') - .find(diff => { - const query = JSON.parse(diff.original!.query) as { ref: string }; - return query.ref !== 'HEAD'; - }); + .filter(diff => diff.original && isGitUri(diff.original)) + .find(diff => fromGitUri(diff.original!).ref !== 'HEAD'); // Working tree + index diff information const diffInformationWorkingTreeAndIndex = textEditor.diffInformation - .filter(diff => diff.original?.scheme === 'git') - .find(diff => { - const query = JSON.parse(diff.original!.query) as { ref: string }; - return query.ref === 'HEAD'; - }); + .filter(diff => diff.original && isGitUri(diff.original)) + .find(diff => fromGitUri(diff.original!).ref === 'HEAD'); // Working tree diff information is not present or it is stale if (!diffInformationWorkingTree || diffInformationWorkingTree.isStale) { @@ -245,7 +239,7 @@ export class GitBlameController { const diffInformation = diffInformationWorkingTreeAndIndex ?? diffInformationWorkingTree; // Git blame information - const resourceBlameInformation = await this._getBlameInformation(textEditor.document.uri); + const resourceBlameInformation = await this._getBlameInformation(textEditor.document.uri, repository.HEAD.commit); if (!resourceBlameInformation) { return; } From 15ccf581604535f50a8fd72cee82d6fd2fc271b3 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 25 Nov 2024 21:54:10 +0100 Subject: [PATCH 116/119] Update chat setup actions to include experimental configuration precondition (#234605) --- .../contrib/chat/browser/chatSetup.contribution.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts b/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts index 002f7502437bd..4452b0b0d2b77 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup.contribution.ts @@ -753,7 +753,10 @@ class ChatSetupTriggerAction extends Action2 { id: ChatSetupTriggerAction.ID, title: ChatSetupTriggerAction.TITLE, f1: true, - precondition: ChatContextKeys.Setup.installed.negate(), + precondition: ContextKeyExpr.and( + ChatContextKeys.Setup.installed.negate(), + ContextKeyExpr.has('config.chat.experimental.offerSetup') + ), menu: { id: MenuId.ChatCommandCenter, group: 'a_first', @@ -786,7 +789,10 @@ class ChatSetupHideAction extends Action2 { id: ChatSetupHideAction.ID, title: ChatSetupHideAction.TITLE, f1: true, - precondition: ChatContextKeys.Setup.installed.negate(), + precondition: ContextKeyExpr.and( + ChatContextKeys.Setup.installed.negate(), + ContextKeyExpr.has('config.chat.experimental.offerSetup') + ), menu: { id: MenuId.ChatCommandCenter, group: 'z_end', From 4e5bcedf37abc6904d1e1e62fc93c93014713fa7 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 25 Nov 2024 14:09:55 -0800 Subject: [PATCH 117/119] Don't clobber other view events with final one Fixes #234474 Fixes #234214 --- .../browser/gpu/fullFileRenderStrategy.ts | 41 +++++++++++++------ 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts b/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts index 591d4708096cb..316e8feedf235 100644 --- a/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts +++ b/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts @@ -10,7 +10,7 @@ import { EditorOption } from '../../common/config/editorOptions.js'; import { CursorColumns } from '../../common/core/cursorColumns.js'; import type { IViewLineTokens } from '../../common/tokens/lineTokens.js'; import { ViewEventHandler } from '../../common/viewEventHandler.js'; -import { ViewEventType, type ViewConfigurationChangedEvent, type ViewDecorationsChangedEvent, type ViewLinesChangedEvent, type ViewLinesDeletedEvent, type ViewLinesInsertedEvent, type ViewScrollChangedEvent, type ViewTokensChangedEvent, type ViewZonesChangedEvent } from '../../common/viewEvents.js'; +import { ViewEventType, type ViewConfigurationChangedEvent, type ViewDecorationsChangedEvent, type ViewLinesChangedEvent, type ViewLinesDeletedEvent, type ViewLinesInsertedEvent, type ViewScrollChangedEvent, type ViewThemeChangedEvent, type ViewTokensChangedEvent, type ViewZonesChangedEvent } from '../../common/viewEvents.js'; import type { ViewportData } from '../../common/viewLayout/viewLinesViewportData.js'; import type { InlineDecoration, ViewLineRenderingData } from '../../common/viewModel.js'; import type { ViewContext } from '../../common/viewModel/viewContext.js'; @@ -41,6 +41,7 @@ const enum CellBufferInfo { type QueuedBufferEvent = ( ViewConfigurationChangedEvent | + ViewDecorationsChangedEvent | ViewLinesDeletedEvent | ViewZonesChangedEvent ); @@ -138,8 +139,10 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend } public override onDecorationsChanged(e: ViewDecorationsChangedEvent): boolean { + // console.log('FullFileRenderStrategy.onDecorationsChanged'); // TODO: Don't clear all cells if we can avoid it this._invalidateAllLines(); + this._queueBufferUpdate(e); return true; } @@ -147,10 +150,7 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend // TODO: This currently fires for the entire viewport whenever scrolling stops // https://github.com/microsoft/vscode/issues/233942 for (const range of e.ranges) { - for (let i = range.fromLineNumber; i <= range.toLineNumber; i++) { - this._upToDateLines[0].delete(i); - this._upToDateLines[1].delete(i); - } + this._invalidateLineRange(range.fromLineNumber, range.toLineNumber); } return true; } @@ -177,10 +177,7 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend } public override onLinesChanged(e: ViewLinesChangedEvent): boolean { - for (let i = e.fromLineNumber; i < e.fromLineNumber + e.count; i++) { - this._upToDateLines[0].delete(i); - this._upToDateLines[1].delete(i); - } + this._invalidateLineRange(e.fromLineNumber, e.fromLineNumber + e.count); return true; } @@ -192,6 +189,11 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend return true; } + public override onThemeChanged(e: ViewThemeChangedEvent): boolean { + this._invalidateAllLines(); + return true; + } + public override onZonesChanged(e: ViewZonesChangedEvent): boolean { this._invalidateAllLines(); @@ -221,6 +223,13 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend } } + private _invalidateLineRange(fromLineNumber: number, toLineNumber: number): void { + for (let i = fromLineNumber; i <= toLineNumber; i++) { + this._upToDateLines[0].delete(i); + this._upToDateLines[1].delete(i); + } + } + reset() { this._invalidateAllLines(); for (const bufferIndex of [0, 1]) { @@ -286,7 +295,15 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend cellBuffer.fill(0); dirtyLineStart = 1; - dirtyLineEnd = this._finalRenderedLine; + dirtyLineEnd = Math.max(dirtyLineEnd, this._finalRenderedLine); + this._finalRenderedLine = 0; + break; + } + case ViewEventType.ViewDecorationsChanged: { + cellBuffer.fill(0); + + dirtyLineStart = 1; + dirtyLineEnd = Math.max(dirtyLineEnd, this._finalRenderedLine); this._finalRenderedLine = 0; break; } @@ -302,7 +319,7 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend // Update dirty lines and final rendered line dirtyLineStart = Math.min(dirtyLineStart, e.fromLineNumber); - dirtyLineEnd = this._finalRenderedLine; + dirtyLineEnd = Math.max(dirtyLineEnd, this._finalRenderedLine); this._finalRenderedLine -= e.toLineNumber - e.fromLineNumber + 1; break; } @@ -312,7 +329,7 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend cellBuffer.fill(0); dirtyLineStart = 1; - dirtyLineEnd = this._finalRenderedLine; + dirtyLineEnd = Math.max(dirtyLineEnd, this._finalRenderedLine); this._finalRenderedLine = 0; break; } From 45d8cdbfb9e67460c48f363d310e6257de691f5d Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 25 Nov 2024 14:44:48 -0800 Subject: [PATCH 118/119] Handle line mapping changed and don't update buffers after decoration change Line mapping is the cause of the problems with folding, for decorations we only want to invalidate the up to date lines cache since it won't affect other lines afaict. Fixes #234614 Part of #234214 --- .../browser/gpu/fullFileRenderStrategy.ts | 55 +++++++------------ .../viewParts/viewLinesGpu/viewLinesGpu.ts | 2 + 2 files changed, 23 insertions(+), 34 deletions(-) diff --git a/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts b/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts index 316e8feedf235..df0879c1969bc 100644 --- a/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts +++ b/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts @@ -10,7 +10,7 @@ import { EditorOption } from '../../common/config/editorOptions.js'; import { CursorColumns } from '../../common/core/cursorColumns.js'; import type { IViewLineTokens } from '../../common/tokens/lineTokens.js'; import { ViewEventHandler } from '../../common/viewEventHandler.js'; -import { ViewEventType, type ViewConfigurationChangedEvent, type ViewDecorationsChangedEvent, type ViewLinesChangedEvent, type ViewLinesDeletedEvent, type ViewLinesInsertedEvent, type ViewScrollChangedEvent, type ViewThemeChangedEvent, type ViewTokensChangedEvent, type ViewZonesChangedEvent } from '../../common/viewEvents.js'; +import { ViewEventType, type ViewConfigurationChangedEvent, type ViewDecorationsChangedEvent, type ViewLineMappingChangedEvent, type ViewLinesChangedEvent, type ViewLinesDeletedEvent, type ViewLinesInsertedEvent, type ViewScrollChangedEvent, type ViewThemeChangedEvent, type ViewTokensChangedEvent, type ViewZonesChangedEvent } from '../../common/viewEvents.js'; import type { ViewportData } from '../../common/viewLayout/viewLinesViewportData.js'; import type { InlineDecoration, ViewLineRenderingData } from '../../common/viewModel.js'; import type { ViewContext } from '../../common/viewModel/viewContext.js'; @@ -41,7 +41,7 @@ const enum CellBufferInfo { type QueuedBufferEvent = ( ViewConfigurationChangedEvent | - ViewDecorationsChangedEvent | + ViewLineMappingChangedEvent | ViewLinesDeletedEvent | ViewZonesChangedEvent ); @@ -139,10 +139,7 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend } public override onDecorationsChanged(e: ViewDecorationsChangedEvent): boolean { - // console.log('FullFileRenderStrategy.onDecorationsChanged'); - // TODO: Don't clear all cells if we can avoid it this._invalidateAllLines(); - this._queueBufferUpdate(e); return true; } @@ -160,12 +157,7 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend // line data up to retain some up to date lines // TODO: This does not invalidate lines that are no longer in the file this._invalidateLinesFrom(e.fromLineNumber); - - // Queue updates that need to happen on the active buffer, not just the cache. This is - // deferred since the active buffer could be locked by the GPU which would block the main - // thread. this._queueBufferUpdate(e); - return true; } @@ -194,12 +186,14 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend return true; } - public override onZonesChanged(e: ViewZonesChangedEvent): boolean { + public override onLineMappingChanged(e: ViewLineMappingChangedEvent): boolean { this._invalidateAllLines(); + this._queueBufferUpdate(e); + return true; + } - // Queue updates that need to happen on the active buffer, not just the cache. This is - // deferred since the active buffer could be locked by the GPU which would block the main - // thread. + public override onZonesChanged(e: ViewZonesChangedEvent): boolean { + this._invalidateAllLines(); this._queueBufferUpdate(e); return true; @@ -290,16 +284,10 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend while (queuedBufferUpdates.length) { const e = queuedBufferUpdates.shift()!; switch (e.type) { - case ViewEventType.ViewConfigurationChanged: { - // TODO: Refine the cases for when we throw away all the data - cellBuffer.fill(0); - - dirtyLineStart = 1; - dirtyLineEnd = Math.max(dirtyLineEnd, this._finalRenderedLine); - this._finalRenderedLine = 0; - break; - } - case ViewEventType.ViewDecorationsChanged: { + // TODO: Refine these cases so we're not throwing away everything + case ViewEventType.ViewConfigurationChanged: + case ViewEventType.ViewLineMappingChanged: + case ViewEventType.ViewZonesChanged: { cellBuffer.fill(0); dirtyLineStart = 1; @@ -323,16 +311,6 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend this._finalRenderedLine -= e.toLineNumber - e.fromLineNumber + 1; break; } - case ViewEventType.ViewZonesChanged: { - // TODO: We could retain render data if we know what view zones changed and how - // Zero out content on all lines - cellBuffer.fill(0); - - dirtyLineStart = 1; - dirtyLineEnd = Math.max(dirtyLineEnd, this._finalRenderedLine); - this._finalRenderedLine = 0; - break; - } } } @@ -343,6 +321,10 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend fillStartIndex = ((y - 1) * this._viewGpuContext.maxGpuCols) * Constants.IndicesPerCell; fillEndIndex = (y * this._viewGpuContext.maxGpuCols) * Constants.IndicesPerCell; cellBuffer.fill(0, fillStartIndex, fillEndIndex); + + dirtyLineStart = Math.min(dirtyLineStart, y); + dirtyLineEnd = Math.max(dirtyLineEnd, y); + continue; } @@ -491,6 +473,11 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend ); } + /** + * Queue updates that need to happen on the active buffer, not just the cache. This will be + * deferred to when the actual cell buffer is changed since the active buffer could be locked by + * the GPU which would block the main thread. + */ private _queueBufferUpdate(e: QueuedBufferEvent) { this._queuedBufferUpdates[0].push(e); this._queuedBufferUpdates[1].push(e); diff --git a/src/vs/editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts b/src/vs/editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts index f483e0eda95e6..65dc315e45cbc 100644 --- a/src/vs/editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts +++ b/src/vs/editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts @@ -385,9 +385,11 @@ export class ViewLinesGpu extends ViewPart implements IViewLines { override onCursorStateChanged(e: viewEvents.ViewCursorStateChangedEvent): boolean { return true; } override onDecorationsChanged(e: viewEvents.ViewDecorationsChangedEvent): boolean { return true; } override onFlushed(e: viewEvents.ViewFlushedEvent): boolean { return true; } + override onLinesChanged(e: viewEvents.ViewLinesChangedEvent): boolean { return true; } override onLinesDeleted(e: viewEvents.ViewLinesDeletedEvent): boolean { return true; } override onLinesInserted(e: viewEvents.ViewLinesInsertedEvent): boolean { return true; } + override onLineMappingChanged(e: viewEvents.ViewLineMappingChangedEvent): boolean { return true; } override onRevealRangeRequest(e: viewEvents.ViewRevealRangeRequestEvent): boolean { return true; } override onScrollChanged(e: viewEvents.ViewScrollChangedEvent): boolean { return true; } override onThemeChanged(e: viewEvents.ViewThemeChangedEvent): boolean { return true; } From 90d883f193e59c64ee30f8284c1a246d1d08c648 Mon Sep 17 00:00:00 2001 From: Osvaldo Ortega Date: Mon, 25 Nov 2024 17:13:58 -0800 Subject: [PATCH 119/119] Allow importing multiple files from the search widget into copilot chat --- src/vs/workbench/contrib/search/browser/searchView.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/search/browser/searchView.ts b/src/vs/workbench/contrib/search/browser/searchView.ts index 697012dbc5a62..2d6479dec60db 100644 --- a/src/vs/workbench/contrib/search/browser/searchView.ts +++ b/src/vs/workbench/contrib/search/browser/searchView.ts @@ -971,12 +971,18 @@ export class SearchView extends ViewPane { e.browserEvent.preventDefault(); e.browserEvent.stopPropagation(); + const context = this.tree.getSelection() || e.element; + let arg; + if (context && context.length > 0) { + arg = e.element; + } + this.contextMenuService.showContextMenu({ menuId: MenuId.SearchContext, - menuActionOptions: { shouldForwardArgs: true }, + menuActionOptions: { shouldForwardArgs: true, arg }, contextKeyService: this.contextKeyService, getAnchor: () => e.anchor, - getActionsContext: () => e.element, + getActionsContext: () => context, }); }