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/118] 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/118] 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 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 003/118] 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 004/118] 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 005/118] 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 006/118] 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 007/118] 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 008/118] 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 009/118] 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 010/118] 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 011/118] 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 012/118] 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 013/118] 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 2ee7ae88b4ebc1cf36b91f117d555ad3e697e779 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Wed, 20 Nov 2024 23:14:16 +0100 Subject: [PATCH 014/118] Git - improve enabliment of the editor decoration (#234296) --- extensions/git/src/blame.ts | 35 +++++++++++++++++++++++------------ 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/extensions/git/src/blame.ts b/extensions/git/src/blame.ts index 73d1a9c4fec39..f2cf8ff75ee63 100644 --- a/extensions/git/src/blame.ts +++ b/extensions/git/src/blame.ts @@ -3,9 +3,9 @@ * 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 } from 'vscode'; +import { DecorationOptions, l10n, Position, Range, TextEditor, TextEditorChange, TextEditorDecorationType, TextEditorChangeKind, ThemeColor, Uri, window, workspace, EventEmitter, ConfigurationChangeEvent } from 'vscode'; import { Model } from './model'; -import { dispose, fromNow, IDisposable, pathEquals, runAndSubscribeEvent } from './util'; +import { dispose, fromNow, IDisposable, pathEquals } from './util'; import { Repository } from './repository'; import { throttle } from './decorators'; import { BlameInformation } from './git'; @@ -274,21 +274,32 @@ class GitBlameEditorDecoration { }); this._disposables.push(this._decorationType); - this._disposables.push(runAndSubscribeEvent(workspace.onDidChangeConfiguration, e => { - if (!e || e?.affectsConfiguration('git.blame.editorDecoration.enabled')) { - for (const textEditor of window.visibleTextEditors) { - this._updateDecorations(textEditor); - } + workspace.onDidChangeConfiguration(this._onDidChangeConfiguration, this, this._disposables); + this._controller.onDidChangeBlameInformation(e => this._updateDecorations(e), this, this._disposables); + } + + private _onDidChangeConfiguration(e: ConfigurationChangeEvent): void { + if (!e.affectsConfiguration('git.blame.editorDecoration.enabled')) { + return; + } + + const enabled = this._isEnabled(); + for (const textEditor of window.visibleTextEditors) { + if (enabled) { + this._updateDecorations(textEditor); + } else { + textEditor.setDecorations(this._decorationType, []); } - })); + } + } - this._controller.onDidChangeBlameInformation(e => this._updateDecorations(e), this, this._disposables); + private _isEnabled(): boolean { + const config = workspace.getConfiguration('git'); + return config.get('blame.editorDecoration.enabled', false); } private _updateDecorations(textEditor: TextEditor): void { - const enabled = workspace.getConfiguration('git').get('blame.editorDecoration.enabled', false); - if (!enabled) { - textEditor.setDecorations(this._decorationType, []); + if (!this._isEnabled()) { return; } From 6306259d8751327372071bc2bc04a27022a02cde Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt Date: Wed, 20 Nov 2024 14:14:37 -0800 Subject: [PATCH 015/118] Have `authentication.clientIdUsage` fire before invoking the provider (#234300) To help us capture the extensions that don't resolve. --- .../api/browser/mainThreadAuthentication.ts | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/src/vs/workbench/api/browser/mainThreadAuthentication.ts b/src/vs/workbench/api/browser/mainThreadAuthentication.ts index a7112276807af..83dab878da91e 100644 --- a/src/vs/workbench/api/browser/mainThreadAuthentication.ts +++ b/src/vs/workbench/api/browser/mainThreadAuthentication.ts @@ -299,10 +299,11 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu } async $getSession(providerId: string, scopes: string[], extensionId: string, extensionName: string, options: AuthenticationGetSessionOptions): Promise { + this.sendClientIdUsageTelemetry(extensionId, providerId, scopes); const session = await this.doGetSession(providerId, scopes, extensionId, extensionName, options); if (session) { - this.sendProviderUsageTelemetry(extensionId, providerId, scopes); + this.sendProviderUsageTelemetry(extensionId, providerId); this.authenticationUsageService.addAccountUsage(providerId, session.account.label, scopes, extensionId, extensionName); } @@ -314,18 +315,18 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu return accounts; } - private sendProviderUsageTelemetry(extensionId: string, providerId: string, scopes: string[]): void { - // TODO@TylerLeonhardt this is a temporary addition to telemetry to understand what extensions are overriding the client id. - // We can use this telemetry to reach out to these extension authors and let them know that they many need configuration changes - // due to the adoption of the Microsoft broker. - // Remove this in a few iterations. + // TODO@TylerLeonhardt this is a temporary addition to telemetry to understand what extensions are overriding the client id. + // We can use this telemetry to reach out to these extension authors and let them know that they many need configuration changes + // due to the adoption of the Microsoft broker. + // Remove this in a few iterations. + private _sentClientIdUsageEvents = new Set(); + private sendClientIdUsageTelemetry(extensionId: string, providerId: string, scopes: string[]): void { const containsVSCodeClientIdScope = scopes.some(scope => scope.startsWith('VSCODE_CLIENT_ID:')); - const key = `${extensionId}|${providerId}|${containsVSCodeClientIdScope}`; - if (this._sentProviderUsageEvents.has(key)) { + if (this._sentClientIdUsageEvents.has(key)) { return; } - + this._sentClientIdUsageEvents.add(key); if (containsVSCodeClientIdScope) { type ClientIdUsageClassification = { owner: 'TylerLeonhardt'; @@ -334,7 +335,13 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu }; this.telemetryService.publicLog2<{ extensionId: string }, ClientIdUsageClassification>('authentication.clientIdUsage', { extensionId }); } + } + private sendProviderUsageTelemetry(extensionId: string, providerId: string): void { + const key = `${extensionId}|${providerId}`; + if (this._sentProviderUsageEvents.has(key)) { + return; + } this._sentProviderUsageEvents.add(key); type AuthProviderUsageClassification = { owner: 'TylerLeonhardt'; From 904c3c234a9c54a08ad73fa37ebfba1310841ad5 Mon Sep 17 00:00:00 2001 From: Osvaldo Ortega Date: Wed, 20 Nov 2024 14:33:54 -0800 Subject: [PATCH 016/118] Small fix for notebook search --- .../search/browser/notebookSearch/notebookSearchModelBase.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/vs/workbench/contrib/search/browser/notebookSearch/notebookSearchModelBase.ts b/src/vs/workbench/contrib/search/browser/notebookSearch/notebookSearchModelBase.ts index d585db9ebb0fa..e3ee4fdba65aa 100644 --- a/src/vs/workbench/contrib/search/browser/notebookSearch/notebookSearchModelBase.ts +++ b/src/vs/workbench/contrib/search/browser/notebookSearch/notebookSearchModelBase.ts @@ -44,7 +44,6 @@ export function isIMatchInNotebook(obj: any): obj is IMatchInNotebook { typeof obj.parent === 'function' && typeof obj.cellParent === 'object' && typeof obj.isWebviewMatch === 'function' && - typeof obj.isReadonly === 'function' && typeof obj.cellIndex === 'number' && (typeof obj.webviewIndex === 'number' || obj.webviewIndex === undefined) && (typeof obj.cell === 'object' || obj.cell === undefined); From 0fda1098c0f91798ec461beb6a0e4b05e7686a25 Mon Sep 17 00:00:00 2001 From: Raymond Zhao <7199958+rzhao271@users.noreply.github.com> Date: Wed, 20 Nov 2024 14:42:09 -0800 Subject: [PATCH 017/118] chore: bump emmet-helper (#234295) --- extensions/emmet/package-lock.json | 6 +++--- extensions/emmet/src/test/completion.test.ts | 15 +++++++++++---- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/extensions/emmet/package-lock.json b/extensions/emmet/package-lock.json index 131ce39675840..cadbf5387a8f8 100644 --- a/extensions/emmet/package-lock.json +++ b/extensions/emmet/package-lock.json @@ -90,9 +90,9 @@ } }, "node_modules/@vscode/emmet-helper": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/@vscode/emmet-helper/-/emmet-helper-2.10.0.tgz", - "integrity": "sha512-UHw1EQRgLbSYkyB73/7wR/IzV6zTBnbzEHuuU4Z6b95HKf2lmeTdGwBIwspWBSRrnIA1TI2x2tetBym6ErA7Gw==", + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@vscode/emmet-helper/-/emmet-helper-2.11.0.tgz", + "integrity": "sha512-QLxjQR3imPZPQltfbWRnHU6JecWTF1QSWhx3GAKQpslx7y3Dp6sIIXhKjiUJ/BR9FX8PVthjr9PD6pNwOJfAzw==", "license": "MIT", "dependencies": { "emmet": "^2.4.3", diff --git a/extensions/emmet/src/test/completion.test.ts b/extensions/emmet/src/test/completion.test.ts index 97ac6ecff3359..4f74ba92e25e0 100644 --- a/extensions/emmet/src/test/completion.test.ts +++ b/extensions/emmet/src/test/completion.test.ts @@ -22,14 +22,14 @@ suite('Tests for completion in CSS embedded in HTML', () => { }); // https://github.com/microsoft/vscode/issues/79766 - test('#79766, correct region determination', async () => { + test('microsoft/vscode#79766, correct region determination', async () => { await testCompletionProvider('html', `
di|
`, [ { label: 'div', documentation: `
|
` } ]); }); // https://github.com/microsoft/vscode/issues/86941 - test('#86941, widows should be completed after width', async () => { + test('microsoft/vscode#86941, widows should be completed after width', async () => { await testCompletionProvider('css', `.foo { wi| }`, [ { label: 'width: ;', documentation: `width: |;` } ]); @@ -56,14 +56,14 @@ suite('Tests for completion in CSS embedded in HTML', () => { }); // https://github.com/microsoft/vscode/issues/117020 - test('#117020, ! at end of abbreviation should have completion', async () => { + test('microsoft/vscode#117020, ! at end of abbreviation should have completion', async () => { await testCompletionProvider('css', `.foo { bdbn!| }`, [ { label: 'border-bottom: none !important;', documentation: `border-bottom: none !important;` } ]); }); // https://github.com/microsoft/vscode/issues/138461 - test('#138461, JSX array noise', async () => { + test('microsoft/vscode#138461, JSX array noise', async () => { await testCompletionProvider('jsx', 'a[i]', undefined); await testCompletionProvider('jsx', 'Component[a b]', undefined); await testCompletionProvider('jsx', '[a, b]', undefined); @@ -71,6 +71,13 @@ suite('Tests for completion in CSS embedded in HTML', () => { { label: '
', documentation: '
|
' } ]); }); + + // https://github.com/microsoft/vscode-emmet-helper/pull/90 + test('microsoft/vscode-emmet-helper#90', async () => { + await testCompletionProvider('html', 'dialog', [ + { label: '', documentation: '|' } + ]); + }); }); interface TestCompletionItem { From d4900d29af387c8be5bfc69957c308b40a743021 Mon Sep 17 00:00:00 2001 From: Osvaldo Ortega Date: Wed, 20 Nov 2024 14:43:50 -0800 Subject: [PATCH 018/118] Making find history default to workspace in insiders --- src/vs/editor/common/config/editorOptions.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index 9f01b9fb5a820..17e0805cbdb5f 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -16,6 +16,7 @@ import { USUAL_WORD_SEPARATORS } from '../core/wordHelper.js'; import * as nls from '../../../nls.js'; import { AccessibilitySupport } from '../../../platform/accessibility/common/accessibility.js'; import { IConfigurationPropertySchema } from '../../../platform/configuration/common/configurationRegistry.js'; +import product from '../../../platform/product/common/product.js'; //#region typed options @@ -1727,7 +1728,7 @@ class EditorFind extends BaseEditorOption Date: Thu, 21 Nov 2024 00:08:33 +0100 Subject: [PATCH 019/118] Git - add git blame status bar item (#234302) * Git - improve enabliment of the editor decoration * Git - add git blame status bar item --- extensions/git/package.json | 9 ++++ extensions/git/package.nls.json | 3 +- extensions/git/src/blame.ts | 78 ++++++++++++++++++++++++++++++++- 3 files changed, 88 insertions(+), 2 deletions(-) diff --git a/extensions/git/package.json b/extensions/git/package.json index c251bcfaa002c..bc8243c2b343c 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -3197,6 +3197,15 @@ "tags": [ "experimental" ] + }, + "git.blame.statusBarItem.enabled": { + "type": "boolean", + "default": false, + "markdownDescription": "%config.blameStatusBarItem.enabled%", + "scope": "resource", + "tags": [ + "experimental" + ] } } }, diff --git a/extensions/git/package.nls.json b/extensions/git/package.nls.json index 96435e6944b23..389c90593467c 100644 --- a/extensions/git/package.nls.json +++ b/extensions/git/package.nls.json @@ -276,7 +276,8 @@ "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.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.", "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 f2cf8ff75ee63..da7d2c3cc4a72 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 } from 'vscode'; +import { DecorationOptions, l10n, Position, Range, TextEditor, TextEditorChange, TextEditorDecorationType, TextEditorChangeKind, ThemeColor, Uri, window, workspace, EventEmitter, ConfigurationChangeEvent, StatusBarItem, StatusBarAlignment } from 'vscode'; import { Model } from './model'; import { dispose, fromNow, IDisposable, pathEquals } from './util'; import { Repository } from './repository'; @@ -113,6 +113,7 @@ export class GitBlameController { constructor(private readonly _model: Model) { this._disposables.push(new GitBlameEditorDecoration(this)); + this._disposables.push(new GitBlameStatusBarItem(this)); this._model.onDidOpenRepository(this._onDidOpenRepository, this, this._disposables); this._model.onDidCloseRepository(this._onDidCloseRepository, this, this._disposables); @@ -336,3 +337,78 @@ class GitBlameEditorDecoration { this._disposables = dispose(this._disposables); } } + +class GitBlameStatusBarItem { + private _statusBarItem: StatusBarItem | undefined; + + private _disposables: IDisposable[] = []; + + constructor(private readonly _controller: GitBlameController) { + workspace.onDidChangeConfiguration(this._onDidChangeConfiguration, this, this._disposables); + window.onDidChangeActiveTextEditor(this._onDidChangeActiveTextEditor, this, this._disposables); + + this._controller.onDidChangeBlameInformation(e => this._updateStatusBarItem(e), this, this._disposables); + } + + private _onDidChangeConfiguration(e: ConfigurationChangeEvent): void { + if (!e.affectsConfiguration('git.blame.statusBarItem.enabled')) { + return; + } + + if (this._isEnabled()) { + if (window.activeTextEditor) { + this._updateStatusBarItem(window.activeTextEditor); + } + } else { + this._statusBarItem?.dispose(); + this._statusBarItem = undefined; + } + } + + private _onDidChangeActiveTextEditor(): void { + if (!this._isEnabled()) { + return; + } + + if (window.activeTextEditor) { + this._updateStatusBarItem(window.activeTextEditor); + } else { + this._statusBarItem?.hide(); + } + } + + private _isEnabled(): boolean { + const config = workspace.getConfiguration('git'); + return config.get('blame.statusBarItem.enabled', false); + } + + private _updateStatusBarItem(textEditor: TextEditor): void { + if (!this._isEnabled() || textEditor !== window.activeTextEditor) { + return; + } + + if (!this._statusBarItem) { + this._statusBarItem = window.createStatusBarItem('git.blame', StatusBarAlignment.Right, 200); + this._disposables.push(this._statusBarItem); + } + + const blameInformation = this._controller.textEditorBlameInformation.get(textEditor); + if (!blameInformation || blameInformation.length === 0) { + this._statusBarItem.hide(); + return; + } + + const statueBarItemText = blameInformation[0] + ? typeof blameInformation[0].blameInformation === 'string' + ? ` ${blameInformation[0].blameInformation}` + : ` ${blameInformation[0].blameInformation.authorName ?? ''} (${fromNow(blameInformation[0].blameInformation.date ?? new Date(), true, true)})` + : ''; + + this._statusBarItem.text = `$(git-commit)${statueBarItemText}`; + this._statusBarItem.show(); + } + + dispose() { + this._disposables = dispose(this._disposables); + } +} From 4b01d0172dc3ebf12dcfed807a90529398a00c50 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Thu, 21 Nov 2024 00:29:54 +0100 Subject: [PATCH 020/118] Git - add git blame status bar item command (#234307) --- extensions/git/src/blame.ts | 18 +++++++++++------- extensions/git/src/commands.ts | 18 ++++++++++++++++++ 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/extensions/git/src/blame.ts b/extensions/git/src/blame.ts index da7d2c3cc4a72..e9bc84661ae82 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 } from 'vscode'; +import { DecorationOptions, l10n, Position, Range, TextEditor, TextEditorChange, TextEditorDecorationType, TextEditorChangeKind, ThemeColor, Uri, window, workspace, EventEmitter, ConfigurationChangeEvent, StatusBarItem, StatusBarAlignment, Command } from 'vscode'; import { Model } from './model'; import { dispose, fromNow, IDisposable, pathEquals } from './util'; import { Repository } from './repository'; @@ -398,13 +398,17 @@ class GitBlameStatusBarItem { return; } - const statueBarItemText = blameInformation[0] - ? typeof blameInformation[0].blameInformation === 'string' - ? ` ${blameInformation[0].blameInformation}` - : ` ${blameInformation[0].blameInformation.authorName ?? ''} (${fromNow(blameInformation[0].blameInformation.date ?? new Date(), true, true)})` - : ''; + if (typeof blameInformation[0].blameInformation === 'string') { + this._statusBarItem.text = `$(git-commit) ${blameInformation[0].blameInformation}`; + } else { + this._statusBarItem.text = `$(git-commit) ${blameInformation[0].blameInformation.authorName ?? ''} (${fromNow(blameInformation[0].blameInformation.date ?? new Date(), true, true)})`; + this._statusBarItem.command = { + title: l10n.t('View Commit'), + command: 'git.statusBar.viewCommit', + arguments: [textEditor.document.uri, blameInformation[0].blameInformation.id] + } satisfies Command; + } - this._statusBarItem.text = `$(git-commit)${statueBarItemText}`; this._statusBarItem.show(); } diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index 59f9d9c9ce59d..84d9b30f7b722 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -4307,6 +4307,24 @@ export class CommandCenter { env.clipboard.writeText(historyItem.message); } + @command('git.statusBar.viewCommit', { repository: true }) + async viewStatusBarCommit(repository: Repository, historyItemId: string): Promise { + if (!repository || !historyItemId) { + return; + } + + const commit = await repository.getCommit(historyItemId); + const title = `${historyItemId.substring(0, 8)} - ${commit.message}`; + const historyItemParentId = commit.parents.length > 0 ? commit.parents[0] : `${historyItemId}^`; + + const multiDiffSourceUri = toGitUri(Uri.file(repository.root), `${historyItemParentId}..${historyItemId}`, { scheme: 'git-commit', }); + + const changes = await repository.diffBetween(historyItemParentId, historyItemId); + const resources = changes.map(c => toMultiFileDiffEditorUris(c, historyItemParentId, historyItemId)); + + await commands.executeCommand('_workbench.openMultiDiffEditor', { multiDiffSourceUri, title, resources }); + } + private createCommand(id: string, key: string, method: Function, options: ScmCommandOptions): (...args: any[]) => any { const result = (...args: any[]) => { let result: Promise; From ce8e5bb7c8ddf0f5bcdb8ce80ccfddda8913f9c5 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Thu, 21 Nov 2024 00:49:29 +0100 Subject: [PATCH 021/118] Show exceeding attachments in chat (#234308) Show exceeding attachments --- .../chat/browser/chatAttachmentModel.ts | 75 ++++++++++++++++++- .../chatReferencesContentPart.ts | 12 +-- .../browser/chatEditing/chatEditingActions.ts | 12 +-- .../browser/chatEditing/chatEditingSession.ts | 12 +-- .../contrib/chat/browser/chatInputPart.ts | 68 ++++++++++++++--- .../contrib/chat/browser/media/chat.css | 6 +- 6 files changed, 147 insertions(+), 38 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatAttachmentModel.ts b/src/vs/workbench/contrib/chat/browser/chatAttachmentModel.ts index 9307985db0a05..3c0544720c6bf 100644 --- a/src/vs/workbench/contrib/chat/browser/chatAttachmentModel.ts +++ b/src/vs/workbench/contrib/chat/browser/chatAttachmentModel.ts @@ -8,6 +8,7 @@ import { Disposable } from '../../../../base/common/lifecycle.js'; import { basename } from '../../../../base/common/resources.js'; import { URI } from '../../../../base/common/uri.js'; import { IRange } from '../../../../editor/common/core/range.js'; +import { IChatEditingService } from '../common/chatEditingService.js'; import { IChatRequestVariableEntry } from '../common/chatModel.js'; export class ChatAttachmentModel extends Disposable { @@ -16,7 +17,7 @@ export class ChatAttachmentModel extends Disposable { return Array.from(this._attachments.values()); } - private _onDidChangeContext = this._register(new Emitter()); + protected _onDidChangeContext = this._register(new Emitter()); readonly onDidChangeContext = this._onDidChangeContext.event; get size(): number { @@ -52,13 +53,18 @@ export class ChatAttachmentModel extends Disposable { } addContext(...attachments: IChatRequestVariableEntry[]) { + let hasAdded = false; + for (const attachment of attachments) { if (!this._attachments.has(attachment.id)) { this._attachments.set(attachment.id, attachment); + hasAdded = true; } } - this._onDidChangeContext.fire(); + if (hasAdded) { + this._onDidChangeContext.fire(); + } } clearAndSetContext(...attachments: IChatRequestVariableEntry[]) { @@ -66,3 +72,68 @@ export class ChatAttachmentModel extends Disposable { this.addContext(...attachments); } } + +export class EditsAttachmentModel extends ChatAttachmentModel { + + private _onFileLimitExceeded = this._register(new Emitter()); + readonly onFileLimitExceeded = this._onFileLimitExceeded.event; + + private get fileAttachments() { + return this.attachments.filter(attachment => attachment.isFile); + } + + private readonly _excludedFileAttachments: IChatRequestVariableEntry[] = []; + get excludedFileAttachments(): IChatRequestVariableEntry[] { + return this._excludedFileAttachments; + } + + constructor( + @IChatEditingService private readonly _chatEditingService: IChatEditingService, + ) { + super(); + } + + private isExcludeFileAttachment(fileAttachmentId: string) { + return this._excludedFileAttachments.some(attachment => attachment.id === fileAttachmentId); + } + + override addContext(...attachments: IChatRequestVariableEntry[]) { + const currentAttachmentIds = this.getAttachmentIDs(); + + const fileAttachments = attachments.filter(attachment => attachment.isFile); + const newFileAttachments = fileAttachments.filter(attachment => !currentAttachmentIds.has(attachment.id)); + const otherAttachments = attachments.filter(attachment => !attachment.isFile); + + const availableFileCount = Math.max(0, this._chatEditingService.editingSessionFileLimit - this.fileAttachments.length); + const fileAttachmentsToBeAdded = newFileAttachments.slice(0, availableFileCount); + + if (newFileAttachments.length > availableFileCount) { + const attachmentsExceedingSize = newFileAttachments.slice(availableFileCount).filter(attachment => !this.isExcludeFileAttachment(attachment.id)); + this._excludedFileAttachments.push(...attachmentsExceedingSize); + this._onDidChangeContext.fire(); + this._onFileLimitExceeded.fire(); + } + + super.addContext(...otherAttachments, ...fileAttachmentsToBeAdded); + } + + override clear(): void { + this._excludedFileAttachments.splice(0, this._excludedFileAttachments.length); + super.clear(); + } + + override delete(variableEntryId: string) { + const excludedFileIndex = this._excludedFileAttachments.findIndex(attachment => attachment.id === variableEntryId); + if (excludedFileIndex !== -1) { + this._excludedFileAttachments.splice(excludedFileIndex, 1); + } + + super.delete(variableEntryId); + + if (this.fileAttachments.length < this._chatEditingService.editingSessionFileLimit) { + const availableFileCount = Math.max(0, this._chatEditingService.editingSessionFileLimit - this.fileAttachments.length); + const reAddAttachments = this._excludedFileAttachments.splice(0, availableFileCount); + super.addContext(...reAddAttachments); + } + } +} diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatReferencesContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatReferencesContentPart.ts index 5afb4bca6153f..42de7cc59a0bd 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatReferencesContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatReferencesContentPart.ts @@ -50,6 +50,7 @@ export interface IChatReferenceListItem extends IChatContentReference { title?: string; description?: string; state?: WorkingSetEntryState; + excluded?: boolean; } export type IChatCollapsibleListItem = IChatReferenceListItem | IChatWarningMessage; @@ -378,13 +379,13 @@ class CollapsibleListRenderer implements IListRenderer { + this._register(this._editorService.onDidVisibleEditorsChange(() => { this._trackCurrentEditorsInWorkingSet(); })); - this._register(this._editorService.onDidCloseEditor((e) => { - this._trackCurrentEditorsInWorkingSet(e); - })); this._register(autorun(reader => { const entries = this.entries.read(reader); entries.forEach(entry => { @@ -171,8 +168,6 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio } private _trackCurrentEditorsInWorkingSet(e?: IEditorCloseEvent) { - const closedEditor = e?.editor.resource?.toString(); - const existingTransientEntries = new ResourceSet(); for (const file of this._workingSet.keys()) { if (this._workingSet.get(file)?.state === WorkingSetEntryState.Transient) { @@ -191,10 +186,7 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio } if (isCodeEditor(activeEditorControl) && activeEditorControl.hasModel()) { const uri = activeEditorControl.getModel().uri; - if (closedEditor === uri.toString()) { - // The editor group service sees recently closed editors? - // Continue, since we want this to be deleted from the working set - } else if (existingTransientEntries.has(uri)) { + if (existingTransientEntries.has(uri)) { existingTransientEntries.delete(uri); } else if (!this._workingSet.has(uri) && !this._removedTransientEntries.has(uri)) { // Don't add as a transient entry if it's already part of the working set diff --git a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts index 6e06775410cc2..76bcd49b9b3b2 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts @@ -12,7 +12,9 @@ import { StandardMouseEvent } from '../../../../base/browser/mouseEvent.js'; import * as aria from '../../../../base/browser/ui/aria/aria.js'; import { Button } from '../../../../base/browser/ui/button/button.js'; import { IHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegate.js'; +import { getBaseLayerHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegate2.js'; import { createInstantHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegateFactory.js'; +import { HoverPosition } from '../../../../base/browser/ui/hover/hoverWidget.js'; import { renderLabelWithIcons } from '../../../../base/browser/ui/iconLabel/iconLabels.js'; import { ProgressBar } from '../../../../base/browser/ui/progressbar/progressbar.js'; import { IAction } from '../../../../base/common/actions.js'; @@ -84,7 +86,7 @@ import { ILanguageModelChatMetadata, ILanguageModelsService } from '../common/la import { CancelAction, ChatModelPickerActionId, ChatSubmitSecondaryAgentAction, IChatExecuteActionContext, ChatSubmitAction } from './actions/chatExecuteActions.js'; import { ImplicitContextAttachmentWidget } from './attachments/implicitContextAttachment.js'; import { IChatWidget } from './chat.js'; -import { ChatAttachmentModel } from './chatAttachmentModel.js'; +import { ChatAttachmentModel, EditsAttachmentModel } from './chatAttachmentModel.js'; import { IDisposableReference } from './chatContentParts/chatCollections.js'; import { CollapsibleListPool, IChatCollapsibleListItem } from './chatContentParts/chatReferencesContentPart.js'; import { ChatDragAndDrop, EditsDragAndDrop } from './chatDragAndDrop.js'; @@ -223,6 +225,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge private readonly _chatEditsActionsDisposables = this._register(new DisposableStore()); private readonly _chatEditsDisposables = this._register(new DisposableStore()); + private readonly _chatEditsFileLimitHover = this._register(new MutableDisposable()); private _chatEditsProgress: ProgressBar | undefined; private _chatEditsListPool: CollapsibleListPool; private _chatEditList: IDisposableReference> | undefined; @@ -281,7 +284,14 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge ) { super(); - this._attachmentModel = this._register(new ChatAttachmentModel()); + if (this.location === ChatAgentLocation.EditingSession) { + this._attachmentModel = this._register(this.instantiationService.createInstance(EditsAttachmentModel)); + this.dnd = this._register(this.instantiationService.createInstance(EditsDragAndDrop, this.attachmentModel, styles)); + } else { + this._attachmentModel = this._register(this.instantiationService.createInstance(ChatAttachmentModel)); + this.dnd = this._register(this.instantiationService.createInstance(ChatDragAndDrop, this.attachmentModel, styles)); + } + this.getInputState = (): IChatInputState => { return { ...getContribsInputState(), @@ -304,7 +314,6 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge })); this._chatEditsListPool = this._register(this.instantiationService.createInstance(CollapsibleListPool, this._onDidChangeVisibility.event, MenuId.ChatEditingWidgetModifiedFilesToolbar)); - this.dnd = this._register(this.instantiationService.createInstance(this.location === ChatAgentLocation.EditingSession ? EditsDragAndDrop : ChatDragAndDrop, this.attachmentModel, styles)); this._hasFileAttachmentContextKey = ChatContextKeys.hasFileAttachments.bindTo(contextKeyService); } @@ -1044,6 +1053,19 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge }); } } + const excludedEntries: IChatCollapsibleListItem[] = []; + for (const excludedAttachment of (this.attachmentModel as EditsAttachmentModel).excludedFileAttachments) { + if (excludedAttachment.isFile && URI.isUri(excludedAttachment.value) && !seenEntries.has(excludedAttachment.value)) { + excludedEntries.push({ + reference: excludedAttachment.value, + state: WorkingSetEntryState.Attached, + kind: 'reference', + excluded: true, + title: localize('chatEditingSession.excludedFile', 'The Working Set file limit has ben reached. {0} is excluded from the Woking Set. Remove other files to make space for {0}.', basename(excludedAttachment.value.path)) + }); + seenEntries.add(excludedAttachment.value); + } + } entries.sort((a, b) => { if (a.kind === 'reference' && b.kind === 'reference') { if (a.state === b.state || a.state === undefined || b.state === undefined) { @@ -1055,14 +1077,18 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge }); let remainingFileEntriesBudget = this.chatEditingService.editingSessionFileLimit; const overviewRegion = innerContainer.querySelector('.chat-editing-session-overview') as HTMLElement ?? dom.append(innerContainer, $('.chat-editing-session-overview')); - const overviewText = overviewRegion.querySelector('span') ?? dom.append(overviewRegion, $('span')); - overviewText.textContent = localize('chatEditingSession.workingSet', 'Working Set'); + const overviewTitle = overviewRegion.querySelector('.working-set-title') as HTMLElement ?? dom.append(overviewRegion, $('.working-set-title')); + const overviewWorkingSet = overviewTitle.querySelector('span') ?? dom.append(overviewTitle, $('span')); + const overviewFileCount = overviewTitle.querySelector('span.working-set-count') ?? dom.append(overviewTitle, $('span.working-set-count')); + + overviewWorkingSet.textContent = localize('chatEditingSession.workingSet', 'Working Set'); // Record the number of entries that the user wanted to add to the working set - this._attemptedWorkingSetEntriesCount = entries.length; + this._attemptedWorkingSetEntriesCount = entries.length + excludedEntries.length; + overviewFileCount.textContent = ''; if (entries.length === 1) { - overviewText.textContent += ' ' + localize('chatEditingSession.oneFile', '(1 file)'); + overviewFileCount.textContent = ' ' + localize('chatEditingSession.oneFile', '(1 file)'); } else if (entries.length >= remainingFileEntriesBudget) { // The user tried to attach too many files, we have to drop anything after the limit const entriesToPreserve: IChatCollapsibleListItem[] = []; @@ -1099,7 +1125,28 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge entries = [...entriesToPreserve, ...newEntriesThatFit, ...suggestedFilesThatFit]; } if (entries.length > 1) { - overviewText.textContent += ' ' + localize('chatEditingSession.manyFiles', '({0} files)', entries.length); + overviewFileCount.textContent = ' ' + localize('chatEditingSession.manyFiles', '({0} files)', entries.length); + } + + if (excludedEntries.length > 0) { + overviewFileCount.textContent = ' ' + localize('chatEditingSession.excludedFiles', '({0} files, {1} excluded)', entries.length, excludedEntries.length); + } + + const fileLimitReached = remainingFileEntriesBudget <= 0; + overviewFileCount.classList.toggle('file-limit-reached', fileLimitReached); + if (fileLimitReached) { + let title = localize('chatEditingSession.fileLimitReached', 'You have reached the maximum number of files that can be added to the working set.'); + title += excludedEntries.length === 1 ? ' ' + localize('chatEditingSession.excludedOneFile', '1 file is excluded from the Working Set.') : ''; + title += excludedEntries.length > 1 ? ' ' + localize('chatEditingSession.excludedSomeFiles', '{0} files are excluded from the Working Set.', excludedEntries.length) : ''; + + this._chatEditsFileLimitHover.value = getBaseLayerHoverDelegate().setupDelayedHover(overviewFileCount as HTMLElement, + { + content: title, + appearance: { showPointer: true, compact: true }, + position: { hoverPosition: HoverPosition.ABOVE } + }); + } else { + this._chatEditsFileLimitHover.clear(); } // Clear out the previous actions (if any) @@ -1166,12 +1213,13 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } const maxItemsShown = 6; - const itemsShown = Math.min(entries.length, maxItemsShown); + const itemsShown = Math.min(entries.length + excludedEntries.length, maxItemsShown); const height = itemsShown * 22; const list = this._chatEditList.object; list.layout(height); list.getHTMLElement().style.height = `${height}px`; list.splice(0, list.length, entries); + list.splice(entries.length, 0, excludedEntries); this._combinedChatEditWorkingSetEntries = coalesce(entries.map((e) => e.kind === 'reference' && URI.isUri(e.reference) ? e.reference : undefined)); const addFilesElement = innerContainer.querySelector('.chat-editing-session-toolbar-actions') as HTMLElement ?? dom.append(innerContainer, $('.chat-editing-session-toolbar-actions')); @@ -1183,7 +1231,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge // Disable the button if the entries that are not suggested exceed the budget button.enabled = remainingFileEntriesBudget > 0; button.label = localize('chatAddFiles', '{0} Add Files...', '$(add)'); - button.setTitle(button.enabled ? localize('addFiles.label', 'Add files to your working set') : localize('addFilesDisabled.label', 'You have reached the maximum number of files that can be added to the working set.')); + button.setTitle(button.enabled ? localize('addFiles.label', 'Add files to your working set') : localize('chatEditingSession.fileLimitReached', 'You have reached the maximum number of files that can be added to the working set.')); this._chatEditsActionsDisposables.add(button.onDidClick(() => { this.commandService.executeCommand('workbench.action.chat.editing.attachFiles', { widget: chatWidget }); })); diff --git a/src/vs/workbench/contrib/chat/browser/media/chat.css b/src/vs/workbench/contrib/chat/browser/media/chat.css index 2d5c017cd0257..35828792a4d45 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/media/chat.css @@ -579,7 +579,7 @@ have to be updated for changes to the rules above, or to support more deeply nes overflow: hidden; } -.interactive-session .chat-editing-session .chat-editing-session-container .chat-editing-session-overview > span { +.interactive-session .chat-editing-session .chat-editing-session-container .chat-editing-session-overview > .working-set-title { color: var(--vscode-descriptionForeground); font-size: 11px; white-space: nowrap; @@ -588,6 +588,10 @@ have to be updated for changes to the rules above, or to support more deeply nes align-content: center; } +.interactive-session .chat-editing-session .chat-editing-session-container .chat-editing-session-overview > .working-set-title .working-set-count.file-limit-reached { + color: var(--vscode-notificationsWarningIcon-foreground); +} + .interactive-session .chat-editing-session .chat-editing-session-container .monaco-progress-container { position: relative; } From fc5cff7bb6abaa6f46bcabef1be49d835b3dc1bd Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Wed, 20 Nov 2024 15:55:47 -0800 Subject: [PATCH 022/118] Allow overriding how markdown links are inserted using the keybinding (#234310) Allows using the `kind` field in the `pasteAs` keybinding to force links to be inserted a certain way, such as as images --- .../src/commands/insertResource.ts | 4 +- .../copyFiles/dropOrPasteResource.ts | 6 +- .../copyFiles/pasteUrlProvider.ts | 9 ++- .../src/languageFeatures/copyFiles/shared.ts | 56 ++++++++++++++----- .../src/test/pasteUrl.test.ts | 24 ++++++-- 5 files changed, 75 insertions(+), 24 deletions(-) diff --git a/extensions/markdown-language-features/src/commands/insertResource.ts b/extensions/markdown-language-features/src/commands/insertResource.ts index 9369141465ee7..61649c08017f5 100644 --- a/extensions/markdown-language-features/src/commands/insertResource.ts +++ b/extensions/markdown-language-features/src/commands/insertResource.ts @@ -6,7 +6,7 @@ import * as vscode from 'vscode'; import { Utils } from 'vscode-uri'; import { Command } from '../commandManager'; -import { createUriListSnippet, mediaFileExtensions } from '../languageFeatures/copyFiles/shared'; +import { createUriListSnippet, linkEditKind, mediaFileExtensions } from '../languageFeatures/copyFiles/shared'; import { coalesce } from '../util/arrays'; import { getParentDocumentUri } from '../util/document'; import { Schemes } from '../util/schemes'; @@ -84,7 +84,7 @@ function createInsertLinkEdit(activeEditor: vscode.TextEditor, selectedFiles: re const snippetEdits = coalesce(activeEditor.selections.map((selection, i): vscode.SnippetTextEdit | undefined => { const selectionText = activeEditor.document.getText(selection); const snippet = createUriListSnippet(activeEditor.document.uri, selectedFiles.map(uri => ({ uri })), { - insertAsMedia: insertAsMedia, + linkKindHint: insertAsMedia ? 'media' : linkEditKind, placeholderText: selectionText, placeholderStartIndex: (i + 1) * selectedFiles.length, separator: insertAsMedia ? '\n' : ' ', diff --git a/extensions/markdown-language-features/src/languageFeatures/copyFiles/dropOrPasteResource.ts b/extensions/markdown-language-features/src/languageFeatures/copyFiles/dropOrPasteResource.ts index 7677bdadcd3b7..f74ff677e0dea 100644 --- a/extensions/markdown-language-features/src/languageFeatures/copyFiles/dropOrPasteResource.ts +++ b/extensions/markdown-language-features/src/languageFeatures/copyFiles/dropOrPasteResource.ts @@ -106,10 +106,10 @@ class ResourcePasteOrDropProvider implements vscode.DocumentPasteEditProvider, v document: vscode.TextDocument, ranges: readonly vscode.Range[], dataTransfer: vscode.DataTransfer, - settings: { + settings: Readonly<{ insert: InsertMarkdownLink; copyIntoWorkspace: CopyFilesSettings; - }, + }>, context: vscode.DocumentPasteEditContext | undefined, token: vscode.CancellationToken, ): Promise { @@ -172,7 +172,7 @@ class ResourcePasteOrDropProvider implements vscode.DocumentPasteEditProvider, v } } - const edit = createInsertUriListEdit(document, ranges, uriList); + const edit = createInsertUriListEdit(document, ranges, uriList, { linkKindHint: context?.only }); if (!edit) { return; } diff --git a/extensions/markdown-language-features/src/languageFeatures/copyFiles/pasteUrlProvider.ts b/extensions/markdown-language-features/src/languageFeatures/copyFiles/pasteUrlProvider.ts index 4cae4f60cf933..a947216fe3272 100644 --- a/extensions/markdown-language-features/src/languageFeatures/copyFiles/pasteUrlProvider.ts +++ b/extensions/markdown-language-features/src/languageFeatures/copyFiles/pasteUrlProvider.ts @@ -29,7 +29,7 @@ class PasteUrlEditProvider implements vscode.DocumentPasteEditProvider { document: vscode.TextDocument, ranges: readonly vscode.Range[], dataTransfer: vscode.DataTransfer, - _context: vscode.DocumentPasteEditContext, + context: vscode.DocumentPasteEditContext, token: vscode.CancellationToken, ): Promise { const pasteUrlSetting = vscode.workspace.getConfiguration('markdown', document) @@ -44,12 +44,17 @@ class PasteUrlEditProvider implements vscode.DocumentPasteEditProvider { return; } + // TODO: If the user has explicitly requested to paste as a markdown link, + // try to paste even if we don't have a valid uri const uriText = findValidUriInText(text); if (!uriText) { return; } - const edit = createInsertUriListEdit(document, ranges, UriList.from(uriText), { preserveAbsoluteUris: true }); + const edit = createInsertUriListEdit(document, ranges, UriList.from(uriText), { + linkKindHint: context.only, + preserveAbsoluteUris: true + }); if (!edit) { return; } diff --git a/extensions/markdown-language-features/src/languageFeatures/copyFiles/shared.ts b/extensions/markdown-language-features/src/languageFeatures/copyFiles/shared.ts index 2d71883dc7853..cdd2476ad382d 100644 --- a/extensions/markdown-language-features/src/languageFeatures/copyFiles/shared.ts +++ b/extensions/markdown-language-features/src/languageFeatures/copyFiles/shared.ts @@ -177,11 +177,11 @@ interface UriListSnippetOptions { readonly placeholderStartIndex?: number; /** - * Controls if a media link (`![](...)`) is inserted instead of a normal markdown link. + * Hints how links should be inserted, e.g. as normal markdown link or as an image. * - * By default tries to infer this from the uri. + * By default this is inferred from the uri. If you use `media`, we will insert the resource as an image, video, or audio. */ - readonly insertAsMedia?: boolean; + readonly linkKindHint?: vscode.DocumentDropOrPasteEditKind | 'media'; readonly separator?: string; @@ -229,12 +229,16 @@ export function createUriListSnippet( uris.forEach((uri, i) => { const mdPath = (!options?.preserveAbsoluteUris ? getRelativeMdPath(documentDir, uri.uri) : undefined) ?? uri.str ?? uri.uri.toString(); - const ext = URI.Utils.extname(uri.uri).toLowerCase().replace('.', ''); - const insertAsMedia = options?.insertAsMedia || (typeof options?.insertAsMedia === 'undefined' && mediaFileExtensions.has(ext)); + const desiredKind = getDesiredLinkKind(uri.uri, options); - if (insertAsMedia) { - const insertAsVideo = mediaFileExtensions.get(ext) === MediaKind.Video; - const insertAsAudio = mediaFileExtensions.get(ext) === MediaKind.Audio; + if (desiredKind === DesiredLinkKind.Link) { + insertedLinkCount++; + snippet.appendText('['); + snippet.appendPlaceholder(escapeBrackets(options?.placeholderText ?? 'text'), placeholderIndex); + snippet.appendText(`](${escapeMarkdownLinkPath(mdPath)})`); + } else { + const insertAsVideo = desiredKind === DesiredLinkKind.Video; + const insertAsAudio = desiredKind === DesiredLinkKind.Audio; if (insertAsVideo || insertAsAudio) { if (insertAsVideo) { insertedVideoCount++; @@ -255,11 +259,6 @@ export function createUriListSnippet( snippet.appendPlaceholder(placeholderText, placeholderIndex); snippet.appendText(`](${escapeMarkdownLinkPath(mdPath)})`); } - } else { - insertedLinkCount++; - snippet.appendText('['); - snippet.appendPlaceholder(escapeBrackets(options?.placeholderText ?? 'text'), placeholderIndex); - snippet.appendText(`](${escapeMarkdownLinkPath(mdPath)})`); } if (i < uris.length - 1 && uris.length > 1) { @@ -270,6 +269,37 @@ export function createUriListSnippet( return { snippet, insertedAudioCount, insertedVideoCount, insertedImageCount, insertedLinkCount }; } +enum DesiredLinkKind { + Link, + Image, + Video, + Audio, +} + +function getDesiredLinkKind(uri: vscode.Uri, options: UriListSnippetOptions | undefined): DesiredLinkKind { + if (options?.linkKindHint instanceof vscode.DocumentDropOrPasteEditKind) { + if (linkEditKind.contains(options.linkKindHint)) { + return DesiredLinkKind.Link; + } else if (imageEditKind.contains(options.linkKindHint)) { + return DesiredLinkKind.Image; + } else if (audioEditKind.contains(options.linkKindHint)) { + return DesiredLinkKind.Audio; + } else if (videoEditKind.contains(options.linkKindHint)) { + return DesiredLinkKind.Video; + } + } + + const normalizedExt = URI.Utils.extname(uri).toLowerCase().replace('.', ''); + if (options?.linkKindHint === 'media' || mediaFileExtensions.has(normalizedExt)) { + switch (mediaFileExtensions.get(normalizedExt)) { + case MediaKind.Video: return DesiredLinkKind.Video; + case MediaKind.Audio: return DesiredLinkKind.Audio; + default: return DesiredLinkKind.Image; + } + } + + return DesiredLinkKind.Link; +} function getRelativeMdPath(dir: vscode.Uri | undefined, file: vscode.Uri): string | undefined { if (dir && dir.scheme === file.scheme && dir.authority === file.authority) { diff --git a/extensions/markdown-language-features/src/test/pasteUrl.test.ts b/extensions/markdown-language-features/src/test/pasteUrl.test.ts index fdb6e6c2f717c..45737c354dad5 100644 --- a/extensions/markdown-language-features/src/test/pasteUrl.test.ts +++ b/extensions/markdown-language-features/src/test/pasteUrl.test.ts @@ -6,7 +6,7 @@ import * as assert from 'assert'; import 'mocha'; import * as vscode from 'vscode'; import { InMemoryDocument } from '../client/inMemoryDocument'; -import { createInsertUriListEdit } from '../languageFeatures/copyFiles/shared'; +import { createInsertUriListEdit, imageEditKind, linkEditKind } from '../languageFeatures/copyFiles/shared'; import { InsertMarkdownLink, findValidUriInText, shouldInsertMarkdownLinkByDefault } from '../languageFeatures/copyFiles/smartDropOrPaste'; import { noopToken } from '../util/cancellation'; import { UriList } from '../util/uriList'; @@ -20,8 +20,6 @@ function makeTestDoc(contents: string) { suite('createEditAddingLinksForUriList', () => { test('Markdown Link Pasting should occur for a valid link (end to end)', async () => { - // createEditAddingLinksForUriList -> checkSmartPaste -> tryGetUriListSnippet -> createUriListSnippet -> createLinkSnippet - const result = createInsertUriListEdit( new InMemoryDocument(vscode.Uri.file('test.md'), 'hello world!'), [new vscode.Range(0, 0, 0, 12)], UriList.from('https://www.microsoft.com/')); // need to check the actual result -> snippet value @@ -110,7 +108,6 @@ suite('createEditAddingLinksForUriList', () => { }); suite('createInsertUriListEdit', () => { - test('Should create snippet with < > when pasted link has an mismatched parentheses', () => { const edit = createInsertUriListEdit(makeTestDoc(''), [new vscode.Range(0, 0, 0, 0)], UriList.from('https://www.mic(rosoft.com')); assert.strictEqual(edit?.edits?.[0].snippet.value, '[${1:text}]()'); @@ -135,6 +132,25 @@ suite('createEditAddingLinksForUriList', () => { const edit = createInsertUriListEdit(makeTestDoc(''), [new vscode.Range(0, 0, 0, 0)], UriList.from('https://www.example.com/path?query=value&another=value#fragment')); assert.strictEqual(edit?.edits?.[0].snippet.value, '[${1:text}](https://www.example.com/path?query=value&another=value#fragment)'); }); + + test('Should add image for image file by default', () => { + const edit = createInsertUriListEdit(makeTestDoc(''), [new vscode.Range(0, 0, 0, 0)], UriList.from('https://www.example.com/cat.png')); + assert.strictEqual(edit?.edits?.[0].snippet.value, '![${1:alt text}](https://www.example.com/cat.png)'); + }); + + test('Should be able to override insert style to use link', () => { + const edit = createInsertUriListEdit(makeTestDoc(''), [new vscode.Range(0, 0, 0, 0)], UriList.from('https://www.example.com/cat.png'), { + linkKindHint: linkEditKind, + }); + assert.strictEqual(edit?.edits?.[0].snippet.value, '[${1:text}](https://www.example.com/cat.png)'); + }); + + test('Should be able to override insert style to use images', () => { + const edit = createInsertUriListEdit(makeTestDoc(''), [new vscode.Range(0, 0, 0, 0)], UriList.from('https://www.example.com/'), { + linkKindHint: imageEditKind, + }); + assert.strictEqual(edit?.edits?.[0].snippet.value, '![${1:alt text}](https://www.example.com/)'); + }); }); From 5b9c6582bee29753e8f7a04ed76a0882d759790b Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Thu, 21 Nov 2024 01:26:28 +0100 Subject: [PATCH 023/118] SCM - fix diff information lookup bug (#234309) --- src/vs/workbench/api/browser/mainThreadEditors.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/api/browser/mainThreadEditors.ts b/src/vs/workbench/api/browser/mainThreadEditors.ts index d55cccb62e6dc..56abf0cb28acc 100644 --- a/src/vs/workbench/api/browser/mainThreadEditors.ts +++ b/src/vs/workbench/api/browser/mainThreadEditors.ts @@ -173,8 +173,9 @@ export class MainThreadTextEditors implements MainThreadTextEditorsShape { return undefined; } - const scmQuickDiffChanges = dirtyDiffModel.mapChanges.get(scmQuickDiff.label) ?? []; - const changes = scmQuickDiffChanges.map(index => dirtyDiffModel.changes[index].change); + const changes = dirtyDiffModel.changes + .filter(change => change.label === scmQuickDiff.label) + .map(change => change.change); return { original: scmQuickDiff.originalResource, From ee21e638bef2ea1a5d4b97f2ca9e3f67f643414e 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 024/118] 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 d79858c114f52abe5e8075746601ecd98f5260e6 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Wed, 20 Nov 2024 20:35:55 -0500 Subject: [PATCH 025/118] 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 bbe24d7f73b87f18670fcf501b74ab832fde5ccb Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Wed, 20 Nov 2024 16:41:27 -0800 Subject: [PATCH 026/118] 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 69acde7458f428f0e6869de8915c9dd995cdda1a Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Wed, 20 Nov 2024 17:03:54 -0800 Subject: [PATCH 027/118] 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 969b5714b4fc54992801dceefc3269ce4e07f8f7 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 21 Nov 2024 08:25:40 +0100 Subject: [PATCH 028/118] 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 ff6d7776ae3cd2837d6239371ad22a2cab2e97d4 Mon Sep 17 00:00:00 2001 From: isidorn Date: Thu, 21 Nov 2024 11:14:43 +0100 Subject: [PATCH 029/118] 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 b69589ce385eff52cdfedd7ab89ec8b1fb98ad16 Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Thu, 21 Nov 2024 11:58:33 +0100 Subject: [PATCH 030/118] 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 17e0805cbdb5f..4f9d5057c7c7b 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -5750,7 +5750,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 eeec4c35abc14d7c28c1540228adcb268adee136 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Thu, 21 Nov 2024 03:05:23 -0800 Subject: [PATCH 031/118] 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 7bb46ece483cf5f8e4f66983abdbc5d1bba9ab9e Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 21 Nov 2024 12:18:02 +0100 Subject: [PATCH 032/118] 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 157567fd8f3fbfc09de0ca643d4b51ce28fb76df 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 033/118] 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 ec8a0199a155a768456529fb123c8e5c81e7216c 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 034/118] 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 268d4cf13befe845b59e2adbe213aae14c1b54d4 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 21 Nov 2024 13:15:29 +0100 Subject: [PATCH 035/118] 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 9508be851891834c4036da28461824c664dfa2c0 Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Thu, 21 Nov 2024 13:23:29 +0100 Subject: [PATCH 036/118] 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 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 037/118] 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 d2b1410df14c70f04fc0c4146cb02cdbbe128e0f Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Thu, 21 Nov 2024 16:10:04 +0100 Subject: [PATCH 038/118] 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 a414275985b8ab3472199f1e2f7bbb437d69ba89 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 21 Nov 2024 16:50:23 +0100 Subject: [PATCH 039/118] 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 b82ed8d1a2ad99729469d3c2257e94eb414c73e9 Mon Sep 17 00:00:00 2001 From: Joyce Er Date: Thu, 21 Nov 2024 08:20:06 -0800 Subject: [PATCH 040/118] 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 b6ce07fb3cd9edec82e31d3473be318074d27ab3 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Thu, 21 Nov 2024 08:26:16 -0800 Subject: [PATCH 041/118] 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 7f05cd2559b723ce24cd56db4df8b6bbb79c825e Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Thu, 21 Nov 2024 17:38:13 +0100 Subject: [PATCH 042/118] 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 b7f6bbe55b92660234d8704e89fbc2fbd2a58b11 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 21 Nov 2024 18:09:20 +0100 Subject: [PATCH 043/118] 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 7662e0314532535862e6fb90dff52cde6bea595a Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Thu, 21 Nov 2024 19:46:47 +0100 Subject: [PATCH 044/118] 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 a5071119aa0f2de196cc0439af95b57d539b5f1e Mon Sep 17 00:00:00 2001 From: Peng Lyu Date: Thu, 21 Nov 2024 10:55:27 -0800 Subject: [PATCH 045/118] 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 d7ab35a98e78953402198337f53c279b37f3bec1 Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt Date: Thu, 21 Nov 2024 10:58:49 -0800 Subject: [PATCH 046/118] `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 137a86be40c0121c48d30104347ddf3c2c7bc150 Mon Sep 17 00:00:00 2001 From: Peng Lyu Date: Thu, 21 Nov 2024 11:02:39 -0800 Subject: [PATCH 047/118] 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 450a3b337f8d592b8b619e23adfd80545d8e6267 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Thu, 21 Nov 2024 11:08:49 -0800 Subject: [PATCH 048/118] 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 69adedd46bef3d8c820f6cb556d22d314a0b0c03 Mon Sep 17 00:00:00 2001 From: Joyce Er Date: Thu, 21 Nov 2024 11:13:25 -0800 Subject: [PATCH 049/118] 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 20ecaf99d3bb45555f65fed830a213061ec7a285 Mon Sep 17 00:00:00 2001 From: Peng Lyu Date: Thu, 21 Nov 2024 11:38:28 -0800 Subject: [PATCH 050/118] 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 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 051/118] 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 3e0bc9e864df15e06dfa5b3e7f079b9d11816931 Mon Sep 17 00:00:00 2001 From: Joyce Er Date: Thu, 21 Nov 2024 12:38:12 -0800 Subject: [PATCH 052/118] 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 ec1e84b5514c8c89ab7f6ea5416ad4fb91e2a650 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 21 Nov 2024 21:49:31 +0100 Subject: [PATCH 053/118] 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 f2fba1b45d47d98ee688de9c84df865c300ab593 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Thu, 21 Nov 2024 16:28:42 -0500 Subject: [PATCH 054/118] 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 927f53de441bf9146cc50d8863e67f45502b41e1 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 055/118] 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 d94c69f4104ec744c798a1f7092738b8a7b59b6b Mon Sep 17 00:00:00 2001 From: Michael Lively Date: Thu, 21 Nov 2024 14:43:47 -0800 Subject: [PATCH 056/118] 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 685b14c7e86fa54e0aebd03ecced5e4531ac2ad7 Mon Sep 17 00:00:00 2001 From: Joyce Er Date: Thu, 21 Nov 2024 16:43:24 -0800 Subject: [PATCH 057/118] 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 12f1491edd574bf3fe9be671b49923cd2f32f3e9 Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt Date: Thu, 21 Nov 2024 16:51:00 -0800 Subject: [PATCH 058/118] 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 5cb3edbfc4b1507a9fe7878ff7bd534a1098a089 Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt Date: Thu, 21 Nov 2024 17:20:12 -0800 Subject: [PATCH 059/118] 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 7457c57570d34b2b1639a36cec95bbb097af4948 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 060/118] 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 90868576241dd25c6c5da64adadc0a09de91a9fe 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 061/118] 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 d594d2c919da545b5a451aa4802d2f6bd9772fd1 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 062/118] 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 cb2c473927853ddb6c637d0141b93525abf229e2 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Fri, 22 Nov 2024 13:55:01 +0100 Subject: [PATCH 063/118] 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 e592087658fbbbaee574f7c40537f2af7d71323a 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 064/118] 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 dcd24d94a99b497e63f29c764ffab030f52339ae 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 065/118] :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 a99cb81d67e9e235f23ed0ded8214f558b706571 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Fri, 22 Nov 2024 14:29:39 +0100 Subject: [PATCH 066/118] :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 549824486c46f40bc58f800727569b54c0f127ce Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Fri, 22 Nov 2024 14:48:45 +0100 Subject: [PATCH 067/118] 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 d8d0ddba9cc4b3c011c17ddb932d1afc9f3f3c4a 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 068/118] 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 eddef09fa757b73ac30da7ed1eca2399ebc854f2 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Fri, 22 Nov 2024 14:36:09 +0000 Subject: [PATCH 069/118] 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 06:55:35 -0800 Subject: [PATCH 070/118] 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 071/118] 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 6de7763887dffc0e87e153842bc038b54ae0dbeb 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 072/118] 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 27687b2229467b1409b51a30f9bd024758feec7e 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 073/118] 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 3067477dd773add66dd83b4b14eeebb14996c275 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Fri, 22 Nov 2024 16:40:25 +0100 Subject: [PATCH 074/118] 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 e6bc2ee20f657fbc8e2ed295da0c8f52bca311f7 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 075/118] 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 7248aaa6e5398eca4cce635a9c8e9520e4a4c2d3 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Fri, 22 Nov 2024 18:50:54 +0100 Subject: [PATCH 076/118] 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 e1de2a458dfb770545489daf499131fd328924e7 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Fri, 22 Nov 2024 10:26:47 -0800 Subject: [PATCH 077/118] 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 3b7e1c668a06dde466954b178d9069d514eac37e Mon Sep 17 00:00:00 2001 From: Riley Bruins Date: Fri, 22 Nov 2024 12:11:59 -0800 Subject: [PATCH 078/118] 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 b35d9d1b36aa24c74134ebe3fac0067eb583166a Mon Sep 17 00:00:00 2001 From: Joyce Er Date: Fri, 22 Nov 2024 12:57:59 -0800 Subject: [PATCH 079/118] 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 7fe5b9523596b7a10282cad8e753132ed1afcef9 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Fri, 22 Nov 2024 13:02:49 -0800 Subject: [PATCH 080/118] 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 0e3d452d51f3ae4c749370fcf773b33733276bf5 Mon Sep 17 00:00:00 2001 From: Joyce Er Date: Fri, 22 Nov 2024 12:59:12 -0800 Subject: [PATCH 081/118] 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 982ab5cac3611388bc75fca0b6af25a373ad92c9 Mon Sep 17 00:00:00 2001 From: Joyce Er Date: Fri, 22 Nov 2024 13:06:18 -0800 Subject: [PATCH 082/118] 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 83857a6ee1f2282aaf27c3c236abcad5699bf292 Mon Sep 17 00:00:00 2001 From: Christof Marti Date: Fri, 22 Nov 2024 22:18:35 +0100 Subject: [PATCH 083/118] 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 f6dd987698dbcf98a07f2e6f436d7353a55a4cb9 Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt Date: Fri, 22 Nov 2024 13:35:49 -0800 Subject: [PATCH 084/118] 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 68cd78865ac8bcb0d22fb293d0ca6caa6c4d2729 Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt Date: Fri, 22 Nov 2024 15:39:17 -0800 Subject: [PATCH 085/118] 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 b64f656c21d3b4b27c6c451f230e76a982496f34 Mon Sep 17 00:00:00 2001 From: Bhavya U Date: Fri, 22 Nov 2024 15:52:06 -0800 Subject: [PATCH 086/118] 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 7ddb65bac890520c4eaf5f73a959ff88ec9bdede Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt Date: Fri, 22 Nov 2024 16:17:07 -0800 Subject: [PATCH 087/118] 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 7bbad412d0c057a3bd8d649df8a4474f2a3aa64d Mon Sep 17 00:00:00 2001 From: Joyce Er Date: Fri, 22 Nov 2024 18:23:00 -0800 Subject: [PATCH 088/118] 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 1ea7d3283e972968f8d0bc9452ac30a5626dfc70 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Fri, 22 Nov 2024 22:33:44 -0500 Subject: [PATCH 089/118] 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 eb41ec10c6000008d30b5f981e769fcc0c134b08 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Sat, 23 Nov 2024 10:26:07 +0100 Subject: [PATCH 090/118] 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 9088a3747d6d1ec3c5d3a0c1149b51ac3843dc7b 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 091/118] 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 213334eb801247fa2632c9ccf204ecb4f1865db1 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 092/118] 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 8eb7fac5658846e35a0399dc65e9a0580d4e4ed7 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Sun, 24 Nov 2024 10:38:19 -0500 Subject: [PATCH 093/118] 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 6864e0b8207dea2c1bb9d781fbdb61c44e7d00e9 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 25 Nov 2024 05:19:38 +0100 Subject: [PATCH 094/118] 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 c87b76b8e07f262010f04ebbb8225b148ad1d494 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 095/118] 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 32f0d9972e071129f8aac50a0ad8107ef0efcb2a Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 25 Nov 2024 10:07:59 +0100 Subject: [PATCH 096/118] 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 d204fc0bb2c1f53d1e009da11fe0c165e0faaf61 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 097/118] 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 a43f284857d15a623ee9cbd2b744d2a16b1b3595 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Mon, 25 Nov 2024 11:15:03 +0100 Subject: [PATCH 098/118] 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 613e188eab7b36573d3009cec04954c2c9af02f5 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Mon, 25 Nov 2024 11:17:51 +0100 Subject: [PATCH 099/118] 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 01c6ed39f11145553e5f1ae0777525b4a02893fc 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 100/118] 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 5b0dc9279a0e114a8e29b058f3105043a8edd477 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 101/118] 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 deda7815917b624c9664721db08160a6b389e9fd Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Mon, 25 Nov 2024 11:40:53 +0100 Subject: [PATCH 102/118] 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 74cefc673cc1d3b906fb882af6f0cc15d1cbdf56 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 25 Nov 2024 12:31:53 +0100 Subject: [PATCH 103/118] 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 dc1d2f425f0f62da45dc088098ca59ab30e8c8f4 Mon Sep 17 00:00:00 2001 From: Christof Marti Date: Mon, 25 Nov 2024 12:36:00 +0100 Subject: [PATCH 104/118] 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 8a0d6ef1adf8424dcf42cfe7362287ddec03315b 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 105/118] 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 d73a4792a4c184b28d7ed62c6c48d8f086805978 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 25 Nov 2024 13:26:26 +0100 Subject: [PATCH 106/118] 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 a43e4d45f64faa28f29951ba12fe45caf8c9aa93 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 107/118] 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 b24a96cf2fedf63e0f98d95746e915c9a2015688 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Mon, 25 Nov 2024 14:10:58 +0100 Subject: [PATCH 108/118] 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 b98bc64e224f32852411bf710f10c8f767b5044f Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Mon, 25 Nov 2024 15:10:30 +0100 Subject: [PATCH 109/118] 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 6c6eb1e16658a0c75cac51f2206546e59e36f5fd Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Mon, 25 Nov 2024 15:23:30 +0100 Subject: [PATCH 110/118] 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 70288e019c7d885bd37509433a338ac4447be166 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 25 Nov 2024 15:38:30 +0100 Subject: [PATCH 111/118] 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 24c8f5625d52519ddfef2bea48d861ba847655ab 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 112/118] 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 465f8696db7228b21440d6f0f4d23fa3520c0138 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 25 Nov 2024 15:46:14 +0100 Subject: [PATCH 113/118] 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 63ab465bdf2c3022dd2112c3c498420093e252e9 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Mon, 25 Nov 2024 16:30:27 +0100 Subject: [PATCH 114/118] 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 8224acaacd2b7bbbf1908ba7af0d9938574a64d9 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 115/118] 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 058d2a8f5766831109eb2e9cb5d36809829f7bb6 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 116/118] 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 a88dcd93cab2abc270fc336fb6395691433ed0a1 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 25 Nov 2024 17:06:57 +0100 Subject: [PATCH 117/118] 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 f08b93ebb98c097f5227882fad2bfcc8958fc6d4 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 118/118] 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 }] }); }