Merge branch 'main' into joh/unique-parrotfish

This commit is contained in:
Johannes Rieken
2024-11-15 09:06:26 +01:00
committed by GitHub
95 changed files with 2725 additions and 1629 deletions
+1 -13
View File
@@ -7,7 +7,7 @@ use std::collections::HashMap;
use cli::commands::args::{
CliCore, Commands, DesktopCodeOptions, ExtensionArgs, ExtensionSubcommand,
InstallExtensionArgs, ListExtensionArgs, UninstallExtensionArgs, DownloadExtensionArgs,
InstallExtensionArgs, ListExtensionArgs, UninstallExtensionArgs,
};
/// Tries to parse the argv using the legacy CLI interface, looking for its
@@ -64,7 +64,6 @@ pub fn try_parse_legacy(
// Now translate them to subcommands.
// --list-extensions -> ext list
// --update-extensions -> update
// --download-extension -> ext download <id>
// --install-extension=id -> ext install <id>
// --uninstall-extension=id -> ext uninstall <id>
// --status -> status
@@ -80,17 +79,6 @@ pub fn try_parse_legacy(
})),
..Default::default()
})
} else if let Some(exts) = args.get("download-extension") {
Some(CliCore {
subcommand: Some(Commands::Extension(ExtensionArgs {
subcommand: ExtensionSubcommand::Download(DownloadExtensionArgs {
id: exts.to_vec(),
location: get_first_arg_value("location"),
}),
desktop_code_options,
})),
..Default::default()
})
} else if let Some(exts) = args.remove("install-extension") {
Some(CliCore {
subcommand: Some(Commands::Extension(ExtensionArgs {
-27
View File
@@ -272,8 +272,6 @@ pub enum ExtensionSubcommand {
Uninstall(UninstallExtensionArgs),
/// Update the installed extensions.
Update,
/// Download an extension.
Download(DownloadExtensionArgs),
}
impl ExtensionSubcommand {
@@ -307,16 +305,6 @@ impl ExtensionSubcommand {
ExtensionSubcommand::Update => {
target.push("--update-extensions".to_string());
}
ExtensionSubcommand::Download(args) => {
for id in args.id.iter() {
target.push(format!("--download-extension={id}"));
}
if let Some(location) = &args.location {
if !location.is_empty() {
target.push(format!("--location={location}"));
}
}
}
}
}
}
@@ -359,21 +347,6 @@ pub struct UninstallExtensionArgs {
pub id: Vec<String>,
}
#[derive(Args, Debug, Clone)]
pub struct DownloadExtensionArgs {
/// Id of the extension to download. The identifier of an
/// extension is '${publisher}.${name}'. Should provide '--location' to specify the location to download the VSIX.
/// To download a specific version provide '@${version}'.
/// For example: 'vscode.csharp@1.2.3'.
#[clap(name = "ext-id")]
pub id: Vec<String>,
/// Specify the location to download the VSIX.
#[clap(long, value_name = "location")]
pub location: Option<String>,
}
#[derive(Args, Debug, Clone)]
pub struct VersionArgs {
#[clap(subcommand)]
@@ -125,7 +125,7 @@ export default class TypeScriptServiceClient extends Disposable implements IType
private _isPromptingAfterCrash = false;
private isRestarting: boolean = false;
private hasServerFatallyCrashedTooManyTimes = false;
private readonly loadingIndicator = this._register(new ServerInitializingIndicator());
private readonly loadingIndicator: ServerInitializingIndicator;
public readonly telemetryReporter: TelemetryReporter;
public readonly bufferSyncSupport: BufferSyncSupport;
@@ -158,6 +158,8 @@ export default class TypeScriptServiceClient extends Disposable implements IType
) {
super();
this.loadingIndicator = this._register(new ServerInitializingIndicator(this));
this.logger = services.logger;
this.tracer = new Tracer(this.logger);
@@ -1254,6 +1256,12 @@ class ServerInitializingIndicator extends Disposable {
private _task?: { project: string; resolve: () => void };
constructor(
private readonly client: ITypeScriptServiceClient,
) {
super();
}
public reset(): void {
if (this._task) {
this._task.resolve();
@@ -1269,15 +1277,28 @@ class ServerInitializingIndicator extends Disposable {
// the incoming project loading task is.
this.reset();
const projectDisplayName = vscode.workspace.asRelativePath(projectName);
const projectDisplayName = this.getProjectDisplayName(projectName);
vscode.window.withProgress({
location: vscode.ProgressLocation.Window,
title: vscode.l10n.t("Initializing project '{0}'", projectDisplayName),
title: vscode.l10n.t("Initializing '{0}'", projectDisplayName),
}, () => new Promise<void>(resolve => {
this._task = { project: projectName, resolve };
}));
}
private getProjectDisplayName(projectName: string): string {
const projectUri = this.client.toResource(projectName);
const relPath = vscode.workspace.asRelativePath(projectUri);
const maxDisplayLength = 60;
if (relPath.length > maxDisplayLength) {
return '...' + relPath.slice(-maxDisplayLength);
}
return relPath;
}
public startedLoadingFile(fileName: string, task: Promise<unknown>): void {
if (!this._task) {
vscode.window.withProgress({
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "code-oss-dev",
"version": "1.96.0",
"distro": "4c96b4c357f6753fff9527bbaf38745be0aee80b",
"distro": "4460d4a3498ebd6eabd7ab5552d3ca2600026f99",
"author": {
"name": "Microsoft Corporation"
},
+1 -1
View File
@@ -49,7 +49,7 @@ _@@APPNAME@@()
--list-extensions --show-versions --install-extension
--uninstall-extension --enable-proposed-api --verbose --log -s
--status -p --performance --prof-startup --disable-extensions
--disable-extension --inspect-extensions --update-extensions --download-extension
--disable-extension --inspect-extensions --update-extensions
--inspect-brk-extensions --disable-gpu' -- "$cur") )
[[ $COMPREPLY == *= ]] && compopt -o nospace
return
+1 -2
View File
@@ -20,9 +20,8 @@ arguments=(
'--category[filters installed extension list by category, when using --list-extensions]'
'--show-versions[show versions of installed extensions, when using --list-extensions]'
'--install-extension[install an extension]:id or path:_files -g "*.vsix(-.)"'
'--uninstall-extension[uninstall an extension]:id'
'--uninstall-extension[uninstall an extension]:id or path:_files -g "*.vsix(-.)"'
'--update-extensions[update the installed extensions]'
'--download-extension[download an extension]:id'
'--enable-proposed-api[enables proposed API features for extensions]::extension id: '
'--verbose[print verbose output (implies --wait)]'
'--log[log level to use]:level [info]:(critical error warn info debug trace off)'
+55 -5
View File
@@ -2,9 +2,16 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Color } from '../common/color.js';
import { FileAccess } from '../common/network.js';
import { URI } from '../common/uri.js';
export type CssFragment = string & { readonly __cssFragment: unique symbol };
function asFragment(raw: string): CssFragment {
return raw as CssFragment;
}
export function asCssValueWithDefault(cssPropertyValue: string | undefined, dflt: string): string {
if (cssPropertyValue !== undefined) {
const variableMatch = cssPropertyValue.match(/^\s*var\((.+)\)$/);
@@ -20,16 +27,59 @@ export function asCssValueWithDefault(cssPropertyValue: string | undefined, dflt
return dflt;
}
export function asCSSPropertyValue(value: string) {
return `'${value.replace(/'/g, '%27')}'`;
export function value(value: string): CssFragment {
const out = value.replaceAll(/[^_\-a-z0-9]/gi, '');
if (out !== value) {
console.warn(`CSS value ${value} modified to ${out} to be safe for CSS`);
}
return asFragment(out);
}
export function stringValue(value: string): CssFragment {
return asFragment(`'${value.replaceAll(/'/g, '\\000027')}'`);
}
/**
* returns url('...')
*/
export function asCSSUrl(uri: URI | null | undefined): string {
export function asCSSUrl(uri: URI | null | undefined): CssFragment {
if (!uri) {
return `url('')`;
return asFragment(`url('')`);
}
return inline`url(${stringValue(FileAccess.uriToBrowserUri(uri).toString(true))})`;
}
export function className(value: string): CssFragment {
const out = CSS.escape(value);
if (out !== value) {
console.warn(`CSS class name ${value} modified to ${out} to be safe for CSS`);
}
return asFragment(out);
}
type InlineCssTemplateValue = CssFragment | Color;
/**
* Template string tag that that constructs a CSS fragment.
*
* All expressions in the template must be css safe values.
*/
export function inline(strings: TemplateStringsArray, ...values: InlineCssTemplateValue[]): CssFragment {
return asFragment(strings.reduce((result, str, i) => {
const value = values[i] || '';
return result + str + value;
}, ''));
}
export class Builder {
private readonly _parts: CssFragment[] = [];
push(...parts: CssFragment[]): void {
this._parts.push(...parts);
}
join(joiner = '\n'): CssFragment {
return asFragment(this._parts.join(joiner));
}
return `url('${FileAccess.uriToBrowserUri(uri).toString(true).replace(/'/g, '%27')}')`;
}
@@ -6,7 +6,7 @@
import { $, append } from '../../dom.js';
import { format } from '../../../common/strings.js';
import './countBadge.css';
import { Disposable, IDisposable, toDisposable } from '../../../common/lifecycle.js';
import { Disposable, IDisposable, MutableDisposable, toDisposable } from '../../../common/lifecycle.js';
import { getBaseLayerHoverDelegate } from '../hover/hoverDelegate2.js';
export interface ICountBadgeOptions {
@@ -33,7 +33,7 @@ export class CountBadge extends Disposable {
private count: number = 0;
private countFormat: string;
private titleFormat: string;
private hover: IDisposable | undefined;
private readonly hover = this._register(new MutableDisposable<IDisposable>());
constructor(container: HTMLElement, private readonly options: ICountBadgeOptions, private readonly styles: ICountBadgeStyles) {
@@ -43,6 +43,7 @@ export class CountBadge extends Disposable {
this.countFormat = this.options.countFormat || '{0}';
this.titleFormat = this.options.titleFormat || '';
this.setCount(this.options.count || 0);
this.updateHover();
}
setCount(count: number) {
@@ -57,14 +58,15 @@ export class CountBadge extends Disposable {
setTitleFormat(titleFormat: string) {
this.titleFormat = titleFormat;
this.updateHover();
this.render();
}
private updateHover(): void {
this.hover?.dispose();
this.hover = undefined;
if (this.titleFormat !== '') {
this.hover = getBaseLayerHoverDelegate().setupDelayedHoverAtMouse(this.element, { content: format(this.titleFormat, this.count), appearance: { compact: true } });
if (this.titleFormat !== '' && !this.hover.value) {
this.hover.value = getBaseLayerHoverDelegate().setupDelayedHoverAtMouse(this.element, () => ({ content: format(this.titleFormat, this.count), appearance: { compact: true } }));
} else if (this.titleFormat === '' && this.hover.value) {
this.hover.value = undefined;
}
}
@@ -77,7 +79,5 @@ export class CountBadge extends Disposable {
if (this.styles.badgeBorder) {
this.element.style.border = `1px solid ${this.styles.badgeBorder}`;
}
this.updateHover();
}
}
-1
View File
@@ -33,7 +33,6 @@ function shouldSpawnCliProcess(argv: NativeParsedArgs): boolean {
return !!argv['install-source']
|| !!argv['list-extensions']
|| !!argv['install-extension']
|| !!argv['download-extension']
|| !!argv['uninstall-extension']
|| !!argv['update-extensions']
|| !!argv['locate-extension']
-8
View File
@@ -282,14 +282,6 @@ class CliMain extends Disposable {
return instantiationService.createInstance(ExtensionManagementCLI, new ConsoleLogger(LogLevel.Info, false)).listExtensions(!!this.argv['show-versions'], this.argv['category'], profileLocation);
}
// Download Extensions
else if (this.argv['download-extension']) {
if (!this.argv['location']) {
throw new Error('The location argument is required to download an extension.');
}
return instantiationService.createInstance(ExtensionManagementCLI, new ConsoleLogger(LogLevel.Info, false)).downloadExtensions(this.argv['download-extension'], URI.parse(this.argv['location']));
}
// Install Extension
else if (this.argv['install-extension'] || this.argv['install-builtin-extension']) {
const installOptions: InstallOptions = { isMachineScoped: !!this.argv['do-not-sync'], installPreReleaseVersion: !!this.argv['pre-release'], profileLocation };
@@ -21,11 +21,13 @@ import { ViewEventHandler } from '../../common/viewEventHandler.js';
import { EditorOption } from '../../common/config/editorOptions.js';
import { NavigationCommandRevealType } from '../coreCommands.js';
import { MouseWheelClassifier } from '../../../base/browser/ui/scrollbar/scrollableElement.js';
import type { ViewLinesGpu } from '../viewParts/viewLinesGpu/viewLinesGpu.js';
export interface IPointerHandlerHelper {
viewDomNode: HTMLElement;
linesContentDomNode: HTMLElement;
viewLinesDomNode: HTMLElement;
viewLinesGpu: ViewLinesGpu | undefined;
focusTextArea(): void;
dispatchTextAreaEvent(event: CustomEvent): void;
@@ -22,6 +22,7 @@ import { PositionAffinity } from '../../common/model.js';
import { InjectedText } from '../../common/modelLineProjectionData.js';
import { Mutable } from '../../../base/common/types.js';
import { Lazy } from '../../../base/common/lazy.js';
import type { ViewLinesGpu } from '../viewParts/viewLinesGpu/viewLinesGpu.js';
const enum HitTestResultType {
Unknown,
@@ -238,6 +239,7 @@ export class HitTestContext {
public readonly viewModel: IViewModel;
public readonly layoutInfo: EditorLayoutInfo;
public readonly viewDomNode: HTMLElement;
public readonly viewLinesGpu: ViewLinesGpu | undefined;
public readonly lineHeight: number;
public readonly stickyTabStops: boolean;
public readonly typicalHalfwidthCharacterWidth: number;
@@ -251,6 +253,7 @@ export class HitTestContext {
const options = context.configuration.options;
this.layoutInfo = options.get(EditorOption.layoutInfo);
this.viewDomNode = viewHelper.viewDomNode;
this.viewLinesGpu = viewHelper.viewLinesGpu;
this.lineHeight = options.get(EditorOption.lineHeight);
this.stickyTabStops = options.get(EditorOption.stickyTabStops);
this.typicalHalfwidthCharacterWidth = options.get(EditorOption.fontInfo).typicalHalfwidthCharacterWidth;
@@ -754,6 +757,32 @@ export class MouseTargetFactory {
const pos = new Position(lineNumber, ctx.viewModel.getLineMaxColumn(lineNumber));
return request.fulfillContentEmpty(pos, detail);
}
} else {
if (ctx.viewLinesGpu) {
const lineNumber = ctx.getLineNumberAtVerticalOffset(request.mouseVerticalOffset);
if (ctx.viewModel.getLineLength(lineNumber) === 0) {
const lineWidth = ctx.getLineWidth(lineNumber);
const detail = createEmptyContentDataInLines(request.mouseContentHorizontalOffset - lineWidth);
return request.fulfillContentEmpty(new Position(lineNumber, 1), detail);
}
const lineWidth = ctx.getLineWidth(lineNumber);
if (request.mouseContentHorizontalOffset >= lineWidth) {
// TODO: This is wrong for RTL
const detail = createEmptyContentDataInLines(request.mouseContentHorizontalOffset - lineWidth);
const pos = new Position(lineNumber, ctx.viewModel.getLineMaxColumn(lineNumber));
return request.fulfillContentEmpty(pos, detail);
}
const position = ctx.viewLinesGpu.getPositionAtCoordinate(lineNumber, request.mouseContentHorizontalOffset);
if (position) {
const detail: IMouseTargetContentTextData = {
injectedText: null,
mightBeForeignElement: false
};
return request.fulfillContentText(position, EditorRange.fromPositions(position, position), detail);
}
}
}
// Do the hit test (if not already done)
@@ -5,11 +5,11 @@
import { getActiveWindow } from '../../../base/browser/dom.js';
import { BugIndicatingError } from '../../../base/common/errors.js';
import { Disposable } from '../../../base/common/lifecycle.js';
import { EditorOption } from '../../common/config/editorOptions.js';
import { CursorColumns } from '../../common/core/cursorColumns.js';
import type { IViewLineTokens } from '../../common/tokens/lineTokens.js';
import type { ViewLinesDeletedEvent } from '../../common/viewEvents.js';
import { ViewEventHandler } from '../../common/viewEventHandler.js';
import type { ViewLinesDeletedEvent, ViewScrollChangedEvent } 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';
@@ -38,7 +38,7 @@ const enum CellBufferInfo {
TextureIndex = 5,
}
export class FullFileRenderStrategy extends Disposable implements IGpuRenderStrategy {
export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRenderStrategy {
readonly wgsl: string = fullFileRenderStrategyWgsl;
@@ -57,8 +57,9 @@ export class FullFileRenderStrategy extends Disposable implements IGpuRenderStra
private _visibleObjectCount: number = 0;
private _finalRenderedLine: number = 0;
private _scrollOffsetBindBuffer!: GPUBuffer;
private _scrollOffsetValueBuffers!: [Float32Array, Float32Array];
private _scrollOffsetBindBuffer: GPUBuffer;
private _scrollOffsetValueBuffer: Float32Array;
private _scrollInitialized: boolean = false;
private readonly _queuedBufferUpdates: [ViewLinesDeletedEvent[], ViewLinesDeletedEvent[]] = [[], []];
@@ -76,6 +77,8 @@ export class FullFileRenderStrategy extends Disposable implements IGpuRenderStra
) {
super();
this._context.addEventHandler(this);
// TODO: Detect when lines have been tokenized and clear _upToDateLines
const fontFamily = this._context.configuration.options.get(EditorOption.fontFamily);
const fontSize = this._context.configuration.options.get(EditorOption.fontSize);
@@ -99,12 +102,26 @@ export class FullFileRenderStrategy extends Disposable implements IGpuRenderStra
size: scrollOffsetBufferSize * Float32Array.BYTES_PER_ELEMENT,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
})).object;
this._scrollOffsetValueBuffers = [
new Float32Array(scrollOffsetBufferSize),
new Float32Array(scrollOffsetBufferSize),
];
this._scrollOffsetValueBuffer = new Float32Array(scrollOffsetBufferSize);
}
// #region Event handlers
public override onLinesDeleted(e: ViewLinesDeletedEvent): boolean {
this._queueBufferUpdate(e);
return true;
}
public override onScrollChanged(e?: ViewScrollChangedEvent): boolean {
const dpr = getActiveWindow().devicePixelRatio;
this._scrollOffsetValueBuffer[0] = (e?.scrollLeft ?? this._context.viewLayout.getCurrentScrollLeft()) * dpr;
this._scrollOffsetValueBuffer[1] = (e?.scrollTop ?? this._context.viewLayout.getCurrentScrollTop()) * dpr;
this._device.queue.writeBuffer(this._scrollOffsetBindBuffer, 0, this._scrollOffsetValueBuffer);
return true;
}
// #endregion
reset() {
for (const bufferIndex of [0, 1]) {
// Zero out buffer and upload to GPU to prevent stale rows from rendering
@@ -122,12 +139,8 @@ export class FullFileRenderStrategy extends Disposable implements IGpuRenderStra
let chars = '';
let y = 0;
let x = 0;
let screenAbsoluteX = 0;
let screenAbsoluteY = 0;
let zeroToOneX = 0;
let zeroToOneY = 0;
let wgslX = 0;
let wgslY = 0;
let absoluteOffsetX = 0;
let absoluteOffsetY = 0;
let xOffset = 0;
let glyph: Readonly<ITextureAtlasPageGlyph>;
let cellIndex = 0;
@@ -145,11 +158,10 @@ export class FullFileRenderStrategy extends Disposable implements IGpuRenderStra
const dpr = getActiveWindow().devicePixelRatio;
// Update scroll offset
const scrollOffsetBuffer = this._scrollOffsetValueBuffers[this._activeDoubleBufferIndex];
scrollOffsetBuffer[0] = this._context.viewLayout.getCurrentScrollLeft() * dpr;
scrollOffsetBuffer[1] = this._context.viewLayout.getCurrentScrollTop() * dpr;
this._device.queue.writeBuffer(this._scrollOffsetBindBuffer, 0, scrollOffsetBuffer);
if (!this._scrollInitialized) {
this.onScrollChanged();
this._scrollInitialized = true;
}
// Update cell data
const cellBuffer = new Float32Array(this._cellValueBuffers[this._activeDoubleBufferIndex]);
@@ -200,29 +212,6 @@ export class FullFileRenderStrategy extends Disposable implements IGpuRenderStra
content = lineData.content;
xOffset = 0;
// See ViewLine#renderLine
// const renderLineInput = new RenderLineInput(
// options.useMonospaceOptimizations,
// options.canUseHalfwidthRightwardsArrow,
// lineData.content,
// lineData.continuesWithWrappedLine,
// lineData.isBasicASCII,
// lineData.containsRTL,
// lineData.minColumn - 1,
// lineData.tokens,
// actualInlineDecorations,
// lineData.tabSize,
// lineData.startVisibleColumn,
// options.spaceWidth,
// options.middotWidth,
// options.wsmiddotWidth,
// options.stopRenderingLineAfter,
// options.renderWhitespace,
// options.renderControlCharacters,
// options.fontLigatures !== EditorFontLigatures.OFF,
// selectionsOnLine
// );
tokens = lineData.tokens;
tokenStartIndex = lineData.minColumn - 1;
tokenEndIndex = 0;
@@ -255,8 +244,8 @@ export class FullFileRenderStrategy extends Disposable implements IGpuRenderStra
glyph = this._viewGpuContext.atlas.getGlyph(this._glyphRasterizer, chars, tokenMetadata);
// TODO: Support non-standard character widths
screenAbsoluteX = Math.round((x + xOffset) * viewLineOptions.spaceWidth * dpr);
screenAbsoluteY = (
absoluteOffsetX = Math.round((x + xOffset) * viewLineOptions.spaceWidth * dpr);
absoluteOffsetY = (
Math.ceil((
// Top of line including line height
viewportData.relativeVerticalOffset[y - viewportData.startLineNumber] +
@@ -264,14 +253,10 @@ export class FullFileRenderStrategy extends Disposable implements IGpuRenderStra
Math.floor((viewportData.lineHeight - this._context.configuration.options.get(EditorOption.fontSize)) / 2)
) * dpr)
);
zeroToOneX = screenAbsoluteX / this._viewGpuContext.canvas.domNode.width;
zeroToOneY = screenAbsoluteY / this._viewGpuContext.canvas.domNode.height;
wgslX = zeroToOneX * 2 - 1;
wgslY = zeroToOneY * 2 - 1;
cellIndex = ((y - 1) * this._viewGpuContext.maxGpuCols + x) * Constants.IndicesPerCell;
cellBuffer[cellIndex + CellBufferInfo.Offset_X] = wgslX;
cellBuffer[cellIndex + CellBufferInfo.Offset_Y] = -wgslY;
cellBuffer[cellIndex + CellBufferInfo.Offset_X] = absoluteOffsetX;
cellBuffer[cellIndex + CellBufferInfo.Offset_Y] = absoluteOffsetY;
cellBuffer[cellIndex + CellBufferInfo.GlyphIndex] = glyph.glyphIndex;
cellBuffer[cellIndex + CellBufferInfo.TextureIndex] = glyph.pageIndex;
}
@@ -325,8 +310,4 @@ export class FullFileRenderStrategy extends Disposable implements IGpuRenderStra
this._queuedBufferUpdates[0].push(e);
this._queuedBufferUpdates[1].push(e);
}
onLinesDeleted(e: ViewLinesDeletedEvent): void {
this._queueBufferUpdate(e);
}
}
@@ -67,7 +67,12 @@ struct VSOutput {
var vsOut: VSOutput;
// Multiple vert.position by 2,-2 to get it into clipspace which ranged from -1 to 1
vsOut.position = vec4f(
(((vert.position * vec2f(2, -2)) / layoutInfo.canvasDims)) * glyph.size + cell.position + ((glyph.origin * vec2f(2, -2)) / layoutInfo.canvasDims) + (((layoutInfo.viewportOffset - scrollOffset.offset * vec2(1, -1)) * 2) / layoutInfo.canvasDims),
// Make everything relative to top left instead of center
vec2f(-1, 1) +
((vert.position * vec2f(2, -2)) / layoutInfo.canvasDims) * glyph.size +
((cell.position * vec2f(2, -2)) / layoutInfo.canvasDims) +
((glyph.origin * vec2f(2, -2)) / layoutInfo.canvasDims) +
(((layoutInfo.viewportOffset - scrollOffset.offset * vec2(1, -1)) * 2) / layoutInfo.canvasDims),
0.0,
1.0
);
+19 -16
View File
@@ -8,7 +8,7 @@ import { Event } from '../../../base/common/event.js';
import { IReference, MutableDisposable } from '../../../base/common/lifecycle.js';
import { EditorOption } from '../../common/config/editorOptions.js';
import { ViewEventHandler } from '../../common/viewEventHandler.js';
import type { ViewScrollChangedEvent } from '../../common/viewEvents.js';
import type { ViewCursorStateChangedEvent, ViewScrollChangedEvent } from '../../common/viewEvents.js';
import type { ViewportData } from '../../common/viewLayout/viewLinesViewportData.js';
import type { ViewContext } from '../../common/viewModel/viewContext.js';
import { GPULifecycle } from './gpuDisposable.js';
@@ -42,7 +42,6 @@ export class RectangleRenderer extends ViewEventHandler {
private _scrollOffsetValueBuffer!: Float32Array;
private _initialized: boolean = false;
private _scrollChanged: boolean = true;
private readonly _shapeCollection: IObjectCollectionBuffer<RectangleRendererEntrySpec> = this._register(createObjectCollectionBuffer([
{ name: 'x' },
@@ -242,29 +241,33 @@ export class RectangleRenderer extends ViewEventHandler {
return this._shapeCollection.createEntry({ x, y, width, height, red, green, blue, alpha });
}
// --- begin event handlers
// #region Event handlers
public override onScrollChanged(e: ViewScrollChangedEvent): boolean {
this._scrollChanged = true;
return super.onScrollChanged(e);
return true;
}
// --- end event handlers
private _update() {
const shapes = this._shapeCollection;
if (shapes.dirtyTracker.isDirty) {
this._device.queue.writeBuffer(this._shapeBindBuffer.value!.object, 0, shapes.buffer, shapes.dirtyTracker.dataOffset, shapes.dirtyTracker.dirtySize! * shapes.view.BYTES_PER_ELEMENT);
shapes.dirtyTracker.clear();
}
// Update scroll offset
if (this._scrollChanged) {
public override onCursorStateChanged(e: ViewCursorStateChangedEvent): boolean {
if (this._device) {
const dpr = getActiveWindow().devicePixelRatio;
this._scrollOffsetValueBuffer[0] = this._context.viewLayout.getCurrentScrollLeft() * dpr;
this._scrollOffsetValueBuffer[1] = this._context.viewLayout.getCurrentScrollTop() * dpr;
this._device.queue.writeBuffer(this._scrollOffsetBindBuffer, 0, this._scrollOffsetValueBuffer);
}
return true;
}
// #endregion
private _update() {
if (!this._device) {
return;
}
const shapes = this._shapeCollection;
if (shapes.dirtyTracker.isDirty) {
this._device.queue.writeBuffer(this._shapeBindBuffer.value!.object, 0, shapes.buffer, shapes.dirtyTracker.dataOffset, shapes.dirtyTracker.dirtySize! * shapes.view.BYTES_PER_ELEMENT);
shapes.dirtyTracker.clear();
}
}
draw(viewportData: ViewportData) {
+9 -1
View File
@@ -329,6 +329,7 @@ export class View extends ViewEventHandler {
viewDomNode: this.domNode.domNode,
linesContentDomNode: this._linesContent.domNode,
viewLinesDomNode: this._viewLines.getDomNode().domNode,
viewLinesGpu: this._viewLinesGpu,
focusTextArea: () => {
this.focus();
@@ -359,11 +360,18 @@ export class View extends ViewEventHandler {
visibleRangeForPosition: (lineNumber: number, column: number) => {
this._flushAccumulatedAndRenderNow();
return this._viewLines.visibleRangeForPosition(new Position(lineNumber, column));
const position = new Position(lineNumber, column);
return this._viewLines.visibleRangeForPosition(position) ?? this._viewLinesGpu?.visibleRangeForPosition(position) ?? null;
},
getLineWidth: (lineNumber: number) => {
this._flushAccumulatedAndRenderNow();
if (this._viewLinesGpu) {
const result = this._viewLinesGpu.getLineWidth(lineNumber);
if (result !== undefined) {
return result;
}
}
return this._viewLines.getLineWidth(lineNumber);
}
};
+16 -1
View File
@@ -80,7 +80,22 @@ export class RenderingContext extends RestrictedRenderingContext {
}
public linesVisibleRangesForRange(range: Range, includeNewLines: boolean): LineVisibleRanges[] | null {
return this._viewLines.linesVisibleRangesForRange(range, includeNewLines) ?? this._viewLinesGpu?.linesVisibleRangesForRange(range, includeNewLines) ?? null;
const domRanges = this._viewLines.linesVisibleRangesForRange(range, includeNewLines);
if (!this._viewLinesGpu) {
return domRanges ?? null;
}
const gpuRanges = this._viewLinesGpu.linesVisibleRangesForRange(range, includeNewLines);
if (!domRanges && !gpuRanges) {
return null;
}
const ranges = [];
if (domRanges) {
ranges.push(...domRanges);
}
if (gpuRanges) {
ranges.push(...gpuRanges);
}
return ranges;
}
public visibleRangeForPosition(position: Position): HorizontalPosition | null {
+2 -1
View File
@@ -37,7 +37,8 @@ export const enum PartFingerprint {
ScrollableElement,
TextArea,
ViewLines,
Minimap
Minimap,
ViewLinesGpu
}
export class PartFingerprints {
@@ -5,25 +5,24 @@
import { getActiveWindow } from '../../../../base/browser/dom.js';
import { BugIndicatingError } from '../../../../base/common/errors.js';
import { autorun } from '../../../../base/common/observable.js';
import { autorun, observableValue, runOnChange } from '../../../../base/common/observable.js';
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
import { ILogService } from '../../../../platform/log/common/log.js';
import { EditorOption } from '../../../common/config/editorOptions.js';
import type { Position } from '../../../common/core/position.js';
import type { Range } from '../../../common/core/range.js';
import type { ViewLinesChangedEvent, ViewLinesDeletedEvent, ViewScrollChangedEvent } from '../../../common/viewEvents.js';
import { Position } from '../../../common/core/position.js';
import { Range } from '../../../common/core/range.js';
import type { ViewportData } from '../../../common/viewLayout/viewLinesViewportData.js';
import type { ViewContext } from '../../../common/viewModel/viewContext.js';
import { TextureAtlasPage } from '../../gpu/atlas/textureAtlasPage.js';
import { FullFileRenderStrategy } from '../../gpu/fullFileRenderStrategy.js';
import { BindingId, type IGpuRenderStrategy } from '../../gpu/gpu.js';
import { GPULifecycle } from '../../gpu/gpuDisposable.js';
import { observeDevicePixelDimensions, quadVertices } from '../../gpu/gpuUtils.js';
import { quadVertices } from '../../gpu/gpuUtils.js';
import { ViewGpuContext } from '../../gpu/viewGpuContext.js';
import { FloatHorizontalRange, HorizontalPosition, IViewLines, LineVisibleRanges, RenderingContext, RestrictedRenderingContext, VisibleRanges } from '../../view/renderingContext.js';
import { FloatHorizontalRange, HorizontalPosition, HorizontalRange, IViewLines, LineVisibleRanges, RenderingContext, RestrictedRenderingContext, VisibleRanges } from '../../view/renderingContext.js';
import { ViewPart } from '../../view/viewPart.js';
import { ViewLineOptions } from '../viewLines/viewLineOptions.js';
import type * as viewEvents from '../../../common/viewEvents.js';
const enum GlyphStorageBufferInfo {
FloatsPerEntry = 2 + 2 + 2,
@@ -60,6 +59,8 @@ export class ViewLinesGpu extends ViewPart implements IViewLines {
private _renderStrategy!: IGpuRenderStrategy;
private _contentLeftObs = observableValue('contentLeft', 0);
constructor(
context: ViewContext,
private readonly _viewGpuContext: ViewGpuContext,
@@ -154,8 +155,11 @@ export class ViewLinesGpu extends ViewPart implements IViewLines {
size: Info.BytesPerEntry,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
}, () => updateBufferValues())).object;
this._register(observeDevicePixelDimensions(this.canvas, getActiveWindow(), (w, h) => {
this._device.queue.writeBuffer(layoutInfoUniformBuffer, 0, updateBufferValues(w, h));
this._register(runOnChange(this._viewGpuContext.canvasDevicePixelDimensions, ({ width, height }) => {
this._device.queue.writeBuffer(layoutInfoUniformBuffer, 0, updateBufferValues(width, height));
}));
this._register(runOnChange(this._contentLeftObs, () => {
this._device.queue.writeBuffer(layoutInfoUniformBuffer, 0, updateBufferValues());
}));
}
@@ -368,20 +372,36 @@ export class ViewLinesGpu extends ViewPart implements IViewLines {
throw new BugIndicatingError('Should not be called');
}
override onLinesChanged(e: ViewLinesChangedEvent): boolean {
// #region Event handlers
// Since ViewLinesGpu currently coordinates rendering to the canvas, it must listen to all
// changed events that any GPU part listens to. This is because any drawing to the canvas will
// clear it for that frame, so all parts must be rendered every time.
//
// Additionally, since this is intrinsically linked to ViewLines, it must also listen to events
// from that side. Luckily rendering is cheap, it's only when uploaded data changes does it
// start to cost.
override onCursorStateChanged(e: viewEvents.ViewCursorStateChangedEvent): boolean { return true; }
override onDecorationsChanged(e: viewEvents.ViewDecorationsChangedEvent): boolean { return true; }
override onFlushed(e: viewEvents.ViewFlushedEvent): boolean { return true; }
override onLinesChanged(e: viewEvents.ViewLinesChangedEvent): boolean { return true; }
override onLinesInserted(e: viewEvents.ViewLinesInsertedEvent): boolean { return true; }
override onRevealRangeRequest(e: viewEvents.ViewRevealRangeRequestEvent): boolean { return true; }
override onScrollChanged(e: viewEvents.ViewScrollChangedEvent): boolean { return true; }
override onThemeChanged(e: viewEvents.ViewThemeChangedEvent): boolean { return true; }
override onZonesChanged(e: viewEvents.ViewZonesChangedEvent): boolean { return true; }
override onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean {
this._contentLeftObs.set(this._context.configuration.options.get(EditorOption.layoutInfo).contentLeft, undefined);
return true;
}
override onLinesDeleted(e: ViewLinesDeletedEvent): boolean {
override onLinesDeleted(e: viewEvents.ViewLinesDeletedEvent): boolean {
this._renderStrategy.onLinesDeleted(e);
return true;
}
override onScrollChanged(e: ViewScrollChangedEvent): boolean {
return true;
}
// subscribe to more events
// #endregion
public renderText(viewportData: ViewportData): void {
if (this._initialized) {
@@ -411,7 +431,6 @@ export class ViewLinesGpu extends ViewPart implements IViewLines {
pass.setBindGroup(0, this._bindGroup);
if (this._renderStrategy?.draw) {
// TODO: Don't draw lines if ViewLinesGpu.canRender is false
this._renderStrategy.draw(pass, viewportData);
} else {
pass.draw(quadVertices.length / 2, visibleObjectCount);
@@ -427,8 +446,65 @@ export class ViewLinesGpu extends ViewPart implements IViewLines {
this._lastViewLineOptions = options;
}
linesVisibleRangesForRange(range: Range, includeNewLines: boolean): LineVisibleRanges[] | null {
return null;
linesVisibleRangesForRange(_range: Range, includeNewLines: boolean): LineVisibleRanges[] | null {
if (!this._lastViewportData) {
return null;
}
const originalEndLineNumber = _range.endLineNumber;
const range = Range.intersectRanges(_range, this._lastViewportData.visibleRange);
if (!range) {
return null;
}
const rendStartLineNumber = this._lastViewportData.startLineNumber;
const rendEndLineNumber = this._lastViewportData.endLineNumber;
const viewportData = this._lastViewportData;
const viewLineOptions = this._lastViewLineOptions;
if (!viewportData || !viewLineOptions) {
return null;
}
const visibleRanges: LineVisibleRanges[] = [];
let nextLineModelLineNumber: number = 0;
if (includeNewLines) {
nextLineModelLineNumber = this._context.viewModel.coordinatesConverter.convertViewPositionToModelPosition(new Position(range.startLineNumber, 1)).lineNumber;
}
for (let lineNumber = range.startLineNumber; lineNumber <= range.endLineNumber; lineNumber++) {
if (lineNumber < rendStartLineNumber || lineNumber > rendEndLineNumber) {
continue;
}
const startColumn = lineNumber === range.startLineNumber ? range.startColumn : 1;
const continuesInNextLine = lineNumber !== range.endLineNumber;
const endColumn = continuesInNextLine ? this._context.viewModel.getLineMaxColumn(lineNumber) : range.endColumn;
const visibleRangesForLine = this._visibleRangesForLineRange(lineNumber, startColumn, endColumn);
if (!visibleRangesForLine) {
continue;
}
if (includeNewLines && lineNumber < originalEndLineNumber) {
const currentLineModelLineNumber = nextLineModelLineNumber;
nextLineModelLineNumber = this._context.viewModel.coordinatesConverter.convertViewPositionToModelPosition(new Position(lineNumber + 1, 1)).lineNumber;
if (currentLineModelLineNumber !== nextLineModelLineNumber) {
visibleRangesForLine.ranges[visibleRangesForLine.ranges.length - 1].width += viewLineOptions.spaceWidth;
}
}
visibleRanges.push(new LineVisibleRanges(visibleRangesForLine.outsideRenderedLine, lineNumber, HorizontalRange.from(visibleRangesForLine.ranges), continuesInNextLine));
}
if (visibleRanges.length === 0) {
return null;
}
return visibleRanges;
}
private _visibleRangesForLineRange(lineNumber: number, startColumn: number, endColumn: number): VisibleRanges | null {
@@ -448,20 +524,19 @@ export class ViewLinesGpu extends ViewPart implements IViewLines {
// Resolve tab widths for this line
const lineData = viewportData.getViewLineRenderingData(lineNumber);
const content = lineData.content;
let startColumnResolvedTabWidth = 0;
let endColumnResolvedTabWidth = 0;
let resolvedStartColumnLeft = 0;
for (let x = 0; x < startColumn - 1; x++) {
startColumnResolvedTabWidth += content[x] === '\t' ? lineData.tabSize : 1;
resolvedStartColumnLeft += content[x] === '\t' ? lineData.tabSize : 1;
}
endColumnResolvedTabWidth = startColumnResolvedTabWidth;
let resolvedRangeWidth = 0;
for (let x = startColumn - 1; x < endColumn - 1; x++) {
endColumnResolvedTabWidth += content[x] === '\t' ? lineData.tabSize : 1;
resolvedRangeWidth += content[x] === '\t' ? lineData.tabSize : 1;
}
// Visible horizontal range in _scaled_ pixels
const result = new VisibleRanges(false, [new FloatHorizontalRange(
startColumnResolvedTabWidth * viewLineOptions.spaceWidth,
endColumnResolvedTabWidth * viewLineOptions.spaceWidth)
resolvedStartColumnLeft * viewLineOptions.spaceWidth,
resolvedRangeWidth * viewLineOptions.spaceWidth)
]);
return result;
@@ -474,4 +549,43 @@ export class ViewLinesGpu extends ViewPart implements IViewLines {
}
return new HorizontalPosition(visibleRanges.outsideRenderedLine, visibleRanges.ranges[0].left);
}
getLineWidth(lineNumber: number): number | undefined {
if (!this._lastViewportData || !this._lastViewLineOptions) {
return undefined;
}
if (!ViewGpuContext.canRender(this._lastViewLineOptions, this._lastViewportData, lineNumber)) {
return undefined;
}
const lineData = this._lastViewportData.getViewLineRenderingData(lineNumber);
const lineRange = this._visibleRangesForLineRange(lineNumber, 1, lineData.maxColumn);
const lastRange = lineRange?.ranges.at(-1);
if (lastRange) {
return lastRange.width;
}
return undefined;
}
getPositionAtCoordinate(lineNumber: number, mouseContentHorizontalOffset: number): Position | undefined {
if (!this._lastViewportData || !this._lastViewLineOptions) {
return undefined;
}
if (!ViewGpuContext.canRender(this._lastViewLineOptions, this._lastViewportData, lineNumber)) {
return undefined;
}
const lineData = this._lastViewportData.getViewLineRenderingData(lineNumber);
const content = lineData.content;
let visualColumn = Math.ceil(mouseContentHorizontalOffset / this._lastViewLineOptions.spaceWidth);
let contentColumn = 0;
while (visualColumn > 0) {
if (visualColumn - (content[contentColumn] === '\t' ? lineData.tabSize : 1) < 0) {
break;
}
visualColumn -= content[contentColumn] === '\t' ? lineData.tabSize : 1;
contentColumn++;
}
return new Position(lineNumber, contentColumn);
}
}
@@ -208,26 +208,32 @@ export class InlineCompletionsModel extends Disposable {
const c = this._source.inlineCompletions.read(reader);
if (!c) { return undefined; }
const cursorPosition = this._primaryPosition.read(reader);
let inlineEditCompletion: InlineCompletionWithUpdatedRange | undefined = undefined;
const filteredCompletions: InlineCompletionWithUpdatedRange[] = [];
let inlineEdit: InlineCompletionWithUpdatedRange | undefined = undefined;
const visibleCompletions: InlineCompletionWithUpdatedRange[] = [];
for (const completion of c.inlineCompletions) {
if (!completion.inlineCompletion.sourceInlineCompletion.isInlineEdit) {
if (completion.isVisible(this.textModel, cursorPosition, reader)) {
filteredCompletions.push(completion);
visibleCompletions.push(completion);
}
} else if (filteredCompletions.length === 0 && completion.inlineCompletion.sourceInlineCompletion.isInlineEdit) {
inlineEditCompletion = completion;
} else {
inlineEdit = completion;
}
}
if (visibleCompletions.length !== 0) {
// Don't show the inline edit if there is a visible completion
inlineEdit = undefined;
}
return {
items: filteredCompletions,
inlineEditCompletion,
inlineCompletions: visibleCompletions,
inlineEdit,
};
});
private readonly _filteredInlineCompletionItems = derivedOpts({ owner: this, equalsFn: itemsEquals() }, reader => {
const c = this._inlineCompletionItems.read(reader);
return c?.items ?? [];
return c?.inlineCompletions ?? [];
});
public readonly selectedInlineCompletionIndex = derived<number>(this, (reader) => {
@@ -295,8 +301,8 @@ export class InlineCompletionsModel extends Disposable {
const model = this.textModel;
const item = this._inlineCompletionItems.read(reader);
if (item?.inlineEditCompletion) {
let edit = item.inlineEditCompletion.toSingleTextEdit(reader);
if (item?.inlineEdit) {
let edit = item.inlineEdit.toSingleTextEdit(reader);
edit = singleTextRemoveCommonPrefix(edit, model);
const cursorPos = this._primaryPosition.read(reader);
@@ -304,13 +310,13 @@ export class InlineCompletionsModel extends Disposable {
const cursorDist = LineRange.fromRange(edit.range).distanceToLine(this._primaryPosition.read(reader).lineNumber);
const disableCollapsing = true;
const currentItemIsCollapsed = !disableCollapsing && (cursorDist > 1 && this._collapsedInlineEditId.read(reader) === item.inlineEditCompletion.semanticId);
const currentItemIsCollapsed = !disableCollapsing && (cursorDist > 1 && this._collapsedInlineEditId.read(reader) === item.inlineEdit.semanticId);
const commands = item.inlineEditCompletion.inlineCompletion.source.inlineCompletions.commands;
const commands = item.inlineEdit.inlineCompletion.source.inlineCompletions.commands;
const renderExplicitly = this._jumpedTo.read(reader);
const inlineEdit = new InlineEdit(edit, currentItemIsCollapsed, renderExplicitly, commands ?? []);
return { kind: 'inlineEdit', inlineEdit, inlineCompletion: item.inlineEditCompletion, edits: [edit], cursorAtInlineEdit };
return { kind: 'inlineEdit', inlineEdit, inlineCompletion: item.inlineEdit, edits: [edit], cursorAtInlineEdit };
}
this._jumpedTo.set(false, undefined);
@@ -193,7 +193,8 @@ async function addRefAndCreateResult(
itemsByHash.set(inlineCompletionItem.hash(), inlineCompletionItem);
if (context.triggerKind === InlineCompletionTriggerKind.Automatic) {
// Stop after first visible inline completion
if (!item.isInlineEdit && context.triggerKind === InlineCompletionTriggerKind.Automatic) {
const minifiedEdit = inlineCompletionItem.toSingleTextEdit().removeCommonPrefix(new TextModelText(model));
if (!minifiedEdit.isEmpty) {
shouldStop = true;
@@ -130,7 +130,7 @@ suite('Decoration Render Options', () => {
// single quote must always be escaped/encoded
s.registerDecorationType('test', 'example', { gutterIconPath: URI.file('c:\\files\\foo\\b\'ar.png') });
assertBackground('file:///c:/files/foo/b%27ar.png', 'vscode-file://vscode-app/c:/files/foo/b%27ar.png');
assertBackground('file:///c:/files/foo/b\\000027ar.png', 'vscode-file://vscode-app/c:/files/foo/b\\000027ar.png');
s.removeDecorationType('example');
} else {
// unix file path (used as string)
@@ -140,12 +140,12 @@ suite('Decoration Render Options', () => {
// single quote must always be escaped/encoded
s.registerDecorationType('test', 'example', { gutterIconPath: URI.file('/Users/foo/b\'ar.png') });
assertBackground('file:///Users/foo/b%27ar.png', 'vscode-file://vscode-app/Users/foo/b%27ar.png');
assertBackground('file:///Users/foo/b\\000027ar.png', 'vscode-file://vscode-app/Users/foo/b\\000027ar.png');
s.removeDecorationType('example');
}
s.registerDecorationType('test', 'example', { gutterIconPath: URI.parse('http://test/pa\'th') });
assert(readStyleSheet(styleSheet).indexOf(`{background:url('http://test/pa%27th') center center no-repeat;}`) > 0);
assert(readStyleSheet(styleSheet).indexOf(`{background:url('http://test/pa\\000027th') center center no-repeat;}`) > 0);
s.removeDecorationType('example');
});
});
@@ -566,7 +566,7 @@ export class AccessibilitySignal {
public static readonly codeActionTriggered = AccessibilitySignal.register({
name: localize('accessibilitySignals.codeActionRequestTriggered', 'Code Action Request Triggered'),
sound: Sound.requestSent,
sound: Sound.voiceRecordingStarted,
legacySoundSettingsKey: 'audioCues.codeActionRequestTriggered',
legacyAnnouncementSettingsKey: 'accessibility.alert.codeActionRequestTriggered',
announcementMessage: localize('accessibility.signals.codeActionRequestTriggered', 'Code Action Request Triggered'),
@@ -576,14 +576,7 @@ export class AccessibilitySignal {
public static readonly codeActionApplied = AccessibilitySignal.register({
name: localize('accessibilitySignals.codeActionApplied', 'Code Action Applied'),
legacySoundSettingsKey: 'audioCues.codeActionApplied',
sound: {
randomOneOf: [
Sound.responseReceived1,
Sound.responseReceived2,
Sound.responseReceived3,
Sound.responseReceived4
]
},
sound: Sound.voiceRecordingStopped,
settingsKey: 'accessibility.signals.codeActionApplied'
});
@@ -296,6 +296,7 @@ export class ConfigurationModel implements IConfigurationModel {
}
export interface ConfigurationParseOptions {
skipUnregistered?: boolean;
scopes?: ConfigurationScope[];
skipRestricted?: boolean;
include?: string[];
@@ -428,14 +429,10 @@ export class ConfigurationModelParser {
restricted.push(...result.restricted);
} else {
const propertySchema = configurationProperties[key];
const scope = propertySchema ? typeof propertySchema.scope !== 'undefined' ? propertySchema.scope : ConfigurationScope.WINDOW : undefined;
if (propertySchema?.restricted) {
restricted.push(key);
}
if (!options.exclude?.includes(key) /* Check exclude */
&& (options.include?.includes(key) /* Check include */
|| ((scope === undefined || options.scopes === undefined || options.scopes.includes(scope)) /* Check scopes */
&& !(options.skipRestricted && propertySchema?.restricted)))) /* Check restricted */ {
if (this.shouldInclude(key, propertySchema, options)) {
raw[key] = properties[key];
} else {
hasExcludedProperties = true;
@@ -445,6 +442,31 @@ export class ConfigurationModelParser {
return { raw, restricted, hasExcludedProperties };
}
private shouldInclude(key: string, propertySchema: IConfigurationPropertySchema | undefined, options: ConfigurationParseOptions): boolean {
if (options.exclude?.includes(key)) {
return false;
}
if (options.include?.includes(key)) {
return true;
}
if (options.skipRestricted && propertySchema?.restricted) {
return false;
}
if (options.skipUnregistered && !propertySchema) {
return false;
}
const scope = propertySchema ? typeof propertySchema.scope !== 'undefined' ? propertySchema.scope : ConfigurationScope.WINDOW : undefined;
if (scope === undefined || options.scopes === undefined) {
return true;
}
return options.scopes.includes(scope);
}
private toOverrides(raw: any, conflictReporter: (message: string) => void): IOverrides[] {
const overrides: IOverrides[] = [];
for (const key of Object.keys(raw)) {
@@ -75,8 +75,6 @@ export interface NativeParsedArgs {
'disable-extensions'?: boolean;
'disable-extension'?: string[]; // undefined or array of 1 or more
'list-extensions'?: boolean;
'download-extension'?: string[];
'location'?: string;
'show-versions'?: boolean;
'category'?: string;
'install-extension'?: string[]; // undefined or array of 1 or more
-2
View File
@@ -97,7 +97,6 @@ export const OPTIONS: OptionDescriptions<Required<NativeParsedArgs>> = {
'list-extensions': { type: 'boolean', cat: 'e', description: localize('listExtensions', "List the installed extensions.") },
'show-versions': { type: 'boolean', cat: 'e', description: localize('showVersions', "Show versions of installed extensions, when using --list-extensions.") },
'category': { type: 'string', allowEmptyValue: true, cat: 'e', description: localize('category', "Filters installed extensions by provided category, when using --list-extensions."), args: 'category' },
'download-extension': { type: 'string[]', cat: 'e', args: 'ext-id', description: localize('downloadExtension', "Downloads the extension VSIX that can be installable. The argument is an identifier of an extension that is '${publisher}.${name}'. To download a specific version provide '@${version}'. For example: 'vscode.csharp@1.2.3'. Should provide '--location' to specify the location to download the VSIX.") },
'install-extension': { type: 'string[]', cat: 'e', args: 'ext-id | path', description: localize('installExtension', "Installs or updates an extension. The argument is either an extension id or a path to a VSIX. The identifier of an extension is '${publisher}.${name}'. Use '--force' argument to update to latest version. To install a specific version provide '@${version}'. For example: 'vscode.csharp@1.2.3'.") },
'pre-release': { type: 'boolean', cat: 'e', description: localize('install prerelease', "Installs the pre-release version of the extension, when using --install-extension") },
'uninstall-extension': { type: 'string[]', cat: 'e', args: 'ext-id', description: localize('uninstallExtension', "Uninstalls an extension.") },
@@ -164,7 +163,6 @@ export const OPTIONS: OptionDescriptions<Required<NativeParsedArgs>> = {
'file-chmod': { type: 'boolean' },
'install-builtin-extension': { type: 'string[]' },
'force': { type: 'boolean' },
'location': { type: 'string' },
'do-not-sync': { type: 'boolean' },
'trace': { type: 'boolean' },
'trace-category-filter': { type: 'string' },
@@ -14,7 +14,6 @@ import { EXTENSION_IDENTIFIER_REGEX, IExtensionGalleryService, IExtensionInfo, I
import { areSameExtensions, getExtensionId, getGalleryExtensionId, getIdAndVersion } from './extensionManagementUtil.js';
import { ExtensionType, EXTENSION_CATEGORIES, IExtensionManifest } from '../../extensions/common/extensions.js';
import { ILogger } from '../../log/common/log.js';
import { IUriIdentityService } from '../../uriIdentity/common/uriIdentity.js';
const notFound = (id: string) => localize('notFound', "Extension '{0}' not found.", id);
@@ -29,7 +28,6 @@ export class ExtensionManagementCLI {
protected readonly logger: ILogger,
@IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService,
@IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService,
@IUriIdentityService private readonly uriIdentityService: IUriIdentityService,
) { }
protected get location(): string | undefined {
@@ -72,42 +70,6 @@ export class ExtensionManagementCLI {
}
}
public async downloadExtensions(extensions: string[], target: URI): Promise<void> {
if (!extensions.length) {
return;
}
this.logger.info(localize('downloadingExtensions', "Downloading extensions..."));
const extensionsInfo: IExtensionInfo[] = [];
for (const extension of extensions) {
const [id, version] = getIdAndVersion(extension);
extensionsInfo.push({ id, version: version !== 'prerelease' ? version : undefined, preRelease: version === 'prerelease' });
}
try {
const galleryExtensions = await this.extensionGalleryService.getExtensions(extensionsInfo, CancellationToken.None);
const targetPlatform = await this.extensionManagementService.getTargetPlatform();
await Promise.allSettled(extensionsInfo.map(async extensionInfo => {
const galleryExtension = galleryExtensions.find(e => areSameExtensions(e.identifier, { id: extensionInfo.id }));
if (!galleryExtension) {
this.logger.error(`${notFound(extensionInfo.id)}\n${useId}`);
return;
}
const compatible = await this.extensionGalleryService.getCompatibleExtension(galleryExtension, !!extensionInfo.hasPreRelease, targetPlatform);
try {
await this.extensionGalleryService.download(compatible ?? galleryExtension, this.uriIdentityService.extUri.joinPath(target, `${galleryExtension.identifier.id}-${galleryExtension.version}.vsix`), InstallOperation.None);
this.logger.info(localize('successDownload', "Extension '{0}' was successfully downloaded.", extensionInfo.id));
} catch (error) {
this.logger.error(localize('error while downloading extension', "Error while downloading extension '{0}': {1}", extensionInfo.id, getErrorMessage(error)));
}
}));
} catch (error) {
this.logger.error(localize('error while downloading extensions', "Error while downloading extensions: {0}", getErrorMessage(error)));
throw error;
}
}
public async installExtensions(extensions: (string | URI)[], builtinExtensions: (string | URI)[], installOptions: InstallOptions, force: boolean): Promise<void> {
const failed: string[] = [];
@@ -20,9 +20,10 @@ import { ExtensionKey, groupByExtension } from '../common/extensionManagementUti
import { fromExtractError } from './extensionManagementUtil.js';
import { IExtensionSignatureVerificationService } from './extensionSignatureVerificationService.js';
import { TargetPlatform } from '../../extensions/common/extensions.js';
import { IFileService, IFileStatWithMetadata } from '../../files/common/files.js';
import { FileOperationResult, IFileService, IFileStatWithMetadata, toFileOperationResult } from '../../files/common/files.js';
import { ILogService } from '../../log/common/log.js';
import { ITelemetryService } from '../../telemetry/common/telemetry.js';
import { IUriIdentityService } from '../../uriIdentity/common/uriIdentity.js';
type RetryDownloadClassification = {
owner: 'sandy081';
@@ -40,6 +41,7 @@ export class ExtensionsDownloader extends Disposable {
private static readonly SignatureArchiveExtension = '.sigzip';
readonly extensionsDownloadDir: URI;
private readonly extensionsTrashDir: URI;
private readonly cache: number;
private readonly cleanUpPromise: Promise<void>;
@@ -49,10 +51,12 @@ export class ExtensionsDownloader extends Disposable {
@IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService,
@IExtensionSignatureVerificationService private readonly extensionSignatureVerificationService: IExtensionSignatureVerificationService,
@ITelemetryService private readonly telemetryService: ITelemetryService,
@IUriIdentityService private readonly uriIdentityService: IUriIdentityService,
@ILogService private readonly logService: ILogService,
) {
super();
this.extensionsDownloadDir = environmentService.extensionsDownloadLocation;
this.extensionsTrashDir = uriIdentityService.extUri.joinPath(environmentService.extensionsDownloadLocation, `.trash`);
this.cache = 20; // Cache 20 downloaded VSIX files
this.cleanUpPromise = this.cleanUp();
}
@@ -132,7 +136,7 @@ export class ExtensionsDownloader extends Disposable {
private async downloadSignatureArchive(extension: IGalleryExtension): Promise<URI> {
try {
const location = joinPath(this.extensionsDownloadDir, `.${generateUuid()}`);
const location = joinPath(this.extensionsDownloadDir, `${this.getName(extension)}${ExtensionsDownloader.SignatureArchiveExtension}`);
const attempts = await this.doDownload(extension, 'sigzip', async () => {
await this.extensionGalleryService.downloadSignatureArchive(extension, location);
try {
@@ -224,7 +228,12 @@ export class ExtensionsDownloader extends Disposable {
async delete(location: URI): Promise<void> {
await this.cleanUpPromise;
await this.fileService.del(location);
const trashRelativePath = this.uriIdentityService.extUri.relativePath(this.extensionsDownloadDir, location);
if (trashRelativePath) {
await this.fileService.move(location, this.uriIdentityService.extUri.joinPath(this.extensionsTrashDir, trashRelativePath), true);
} else {
await this.fileService.del(location);
}
}
private async cleanUp(): Promise<void> {
@@ -233,6 +242,15 @@ export class ExtensionsDownloader extends Disposable {
this.logService.trace('Extension VSIX downloads cache dir does not exist');
return;
}
try {
await this.fileService.del(this.extensionsTrashDir, { recursive: true });
} catch (error) {
if (toFileOperationResult(error) !== FileOperationResult.FILE_NOT_FOUND) {
this.logService.error(error);
}
}
const folderStat = await this.fileService.resolve(this.extensionsDownloadDir, { resolveMetadata: true });
if (folderStat.children) {
const toDelete: URI[] = [];
@@ -272,7 +290,7 @@ export class ExtensionsDownloader extends Disposable {
}
private getName(extension: IGalleryExtension): string {
return this.cache ? ExtensionKey.create(extension).toString().toLowerCase() : generateUuid();
return ExtensionKey.create(extension).toString().toLowerCase();
}
}
@@ -332,6 +332,16 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi
},
false,
token);
if (verificationStatus !== ExtensionSignatureVerificationCode.Success && this.environmentService.isBuilt) {
try {
await this.extensionsDownloader.delete(location);
} catch (e) {
/* Ignore */
this.logService.warn(`Error while deleting the downloaded file`, location.toString(), getErrorMessage(e));
}
}
return { local, verificationStatus };
} catch (error) {
try {
@@ -352,6 +362,13 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi
const { location, verificationStatus } = await this.extensionsDownloader.download(extension, operation, verifySignature, clientTargetPlatform);
if (verificationStatus !== ExtensionSignatureVerificationCode.Success && verifySignature && this.environmentService.isBuilt && !isLinux) {
try {
await this.extensionsDownloader.delete(location);
} catch (e) {
/* Ignore */
this.logService.warn(`Error while deleting the downloaded file`, location.toString(), getErrorMessage(e));
}
if (!extension.isSigned) {
throw new ExtensionManagementError(nls.localize('not signed', "Extension is not signed."), ExtensionManagementErrorCode.PackageNotSigned);
}
@@ -22,6 +22,8 @@ import { FileService } from '../../../files/common/fileService.js';
import { InMemoryFileSystemProvider } from '../../../files/common/inMemoryFilesystemProvider.js';
import { TestInstantiationService } from '../../../instantiation/test/common/instantiationServiceMock.js';
import { ILogService, NullLogService } from '../../../log/common/log.js';
import { IUriIdentityService } from '../../../uriIdentity/common/uriIdentity.js';
import { UriIdentityService } from '../../../uriIdentity/common/uriIdentityService.js';
const ROOT = URI.file('tests').with({ scheme: 'vscode-tests' });
@@ -67,6 +69,7 @@ suite('ExtensionDownloader Tests', () => {
instantiationService.stub(ILogService, logService);
instantiationService.stub(IFileService, fileService);
instantiationService.stub(ILogService, logService);
instantiationService.stub(IUriIdentityService, disposables.add(new UriIdentityService(fileService)));
instantiationService.stub(INativeEnvironmentService, { extensionsDownloadLocation: joinPath(ROOT, 'CachedExtensionVSIXs') });
instantiationService.stub(IExtensionGalleryService, {
async download(extension, location, operation) {
@@ -238,10 +238,6 @@ const _allApiProposals = {
languageStatusText: {
proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.languageStatusText.d.ts',
},
lmTools: {
proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.lmTools.d.ts',
version: 15
},
mappedEditsProvider: {
proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.mappedEditsProvider.d.ts',
},
@@ -398,6 +394,9 @@ const _allApiProposals = {
tunnels: {
proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.tunnels.d.ts',
},
valueSelectionInQuickPick: {
proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.valueSelectionInQuickPick.d.ts',
},
workspaceTrust: {
proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.workspaceTrust.d.ts',
}
+30 -24
View File
@@ -259,6 +259,13 @@ export abstract class AbstractLogger extends Disposable implements ILogger {
return this.level !== LogLevel.Off && this.level <= level;
}
protected canLog(level: LogLevel): boolean {
if (this._store.isDisposed) {
return false;
}
return this.checkLogLevel(level);
}
abstract trace(message: string, ...args: any[]): void;
abstract debug(message: string, ...args: any[]): void;
abstract info(message: string, ...args: any[]): void;
@@ -269,8 +276,6 @@ export abstract class AbstractLogger extends Disposable implements ILogger {
export abstract class AbstractMessageLogger extends AbstractLogger implements ILogger {
protected abstract log(level: LogLevel, message: string): void;
constructor(private readonly logAlways?: boolean) {
super();
}
@@ -280,32 +285,31 @@ export abstract class AbstractMessageLogger extends AbstractLogger implements IL
}
trace(message: string, ...args: any[]): void {
if (this.checkLogLevel(LogLevel.Trace)) {
if (this.canLog(LogLevel.Trace)) {
this.log(LogLevel.Trace, format([message, ...args], true));
}
}
debug(message: string, ...args: any[]): void {
if (this.checkLogLevel(LogLevel.Debug)) {
if (this.canLog(LogLevel.Debug)) {
this.log(LogLevel.Debug, format([message, ...args]));
}
}
info(message: string, ...args: any[]): void {
if (this.checkLogLevel(LogLevel.Info)) {
if (this.canLog(LogLevel.Info)) {
this.log(LogLevel.Info, format([message, ...args]));
}
}
warn(message: string, ...args: any[]): void {
if (this.checkLogLevel(LogLevel.Warning)) {
if (this.canLog(LogLevel.Warning)) {
this.log(LogLevel.Warning, format([message, ...args]));
}
}
error(message: string | Error, ...args: any[]): void {
if (this.checkLogLevel(LogLevel.Error)) {
if (this.canLog(LogLevel.Error)) {
if (message instanceof Error) {
const array = Array.prototype.slice.call(arguments) as any[];
array[0] = message.stack;
@@ -317,6 +321,8 @@ export abstract class AbstractMessageLogger extends AbstractLogger implements IL
}
flush(): void { }
protected abstract log(level: LogLevel, message: string): void;
}
@@ -331,7 +337,7 @@ export class ConsoleMainLogger extends AbstractLogger implements ILogger {
}
trace(message: string, ...args: any[]): void {
if (this.checkLogLevel(LogLevel.Trace)) {
if (this.canLog(LogLevel.Trace)) {
if (this.useColors) {
console.log(`\x1b[90m[main ${now()}]\x1b[0m`, message, ...args);
} else {
@@ -341,7 +347,7 @@ export class ConsoleMainLogger extends AbstractLogger implements ILogger {
}
debug(message: string, ...args: any[]): void {
if (this.checkLogLevel(LogLevel.Debug)) {
if (this.canLog(LogLevel.Debug)) {
if (this.useColors) {
console.log(`\x1b[90m[main ${now()}]\x1b[0m`, message, ...args);
} else {
@@ -351,7 +357,7 @@ export class ConsoleMainLogger extends AbstractLogger implements ILogger {
}
info(message: string, ...args: any[]): void {
if (this.checkLogLevel(LogLevel.Info)) {
if (this.canLog(LogLevel.Info)) {
if (this.useColors) {
console.log(`\x1b[90m[main ${now()}]\x1b[0m`, message, ...args);
} else {
@@ -361,7 +367,7 @@ export class ConsoleMainLogger extends AbstractLogger implements ILogger {
}
warn(message: string | Error, ...args: any[]): void {
if (this.checkLogLevel(LogLevel.Warning)) {
if (this.canLog(LogLevel.Warning)) {
if (this.useColors) {
console.warn(`\x1b[93m[main ${now()}]\x1b[0m`, message, ...args);
} else {
@@ -371,7 +377,7 @@ export class ConsoleMainLogger extends AbstractLogger implements ILogger {
}
error(message: string, ...args: any[]): void {
if (this.checkLogLevel(LogLevel.Error)) {
if (this.canLog(LogLevel.Error)) {
if (this.useColors) {
console.error(`\x1b[91m[main ${now()}]\x1b[0m`, message, ...args);
} else {
@@ -394,7 +400,7 @@ export class ConsoleLogger extends AbstractLogger implements ILogger {
}
trace(message: string, ...args: any[]): void {
if (this.checkLogLevel(LogLevel.Trace)) {
if (this.canLog(LogLevel.Trace)) {
if (this.useColors) {
console.log('%cTRACE', 'color: #888', message, ...args);
} else {
@@ -404,7 +410,7 @@ export class ConsoleLogger extends AbstractLogger implements ILogger {
}
debug(message: string, ...args: any[]): void {
if (this.checkLogLevel(LogLevel.Debug)) {
if (this.canLog(LogLevel.Debug)) {
if (this.useColors) {
console.log('%cDEBUG', 'background: #eee; color: #888', message, ...args);
} else {
@@ -414,7 +420,7 @@ export class ConsoleLogger extends AbstractLogger implements ILogger {
}
info(message: string, ...args: any[]): void {
if (this.checkLogLevel(LogLevel.Info)) {
if (this.canLog(LogLevel.Info)) {
if (this.useColors) {
console.log('%c INFO', 'color: #33f', message, ...args);
} else {
@@ -424,7 +430,7 @@ export class ConsoleLogger extends AbstractLogger implements ILogger {
}
warn(message: string | Error, ...args: any[]): void {
if (this.checkLogLevel(LogLevel.Warning)) {
if (this.canLog(LogLevel.Warning)) {
if (this.useColors) {
console.log('%c WARN', 'color: #993', message, ...args);
} else {
@@ -434,7 +440,7 @@ export class ConsoleLogger extends AbstractLogger implements ILogger {
}
error(message: string, ...args: any[]): void {
if (this.checkLogLevel(LogLevel.Error)) {
if (this.canLog(LogLevel.Error)) {
if (this.useColors) {
console.log('%c ERR', 'color: #f33', message, ...args);
} else {
@@ -457,31 +463,31 @@ export class AdapterLogger extends AbstractLogger implements ILogger {
}
trace(message: string, ...args: any[]): void {
if (this.checkLogLevel(LogLevel.Trace)) {
if (this.canLog(LogLevel.Trace)) {
this.adapter.log(LogLevel.Trace, [this.extractMessage(message), ...args]);
}
}
debug(message: string, ...args: any[]): void {
if (this.checkLogLevel(LogLevel.Debug)) {
if (this.canLog(LogLevel.Debug)) {
this.adapter.log(LogLevel.Debug, [this.extractMessage(message), ...args]);
}
}
info(message: string, ...args: any[]): void {
if (this.checkLogLevel(LogLevel.Info)) {
if (this.canLog(LogLevel.Info)) {
this.adapter.log(LogLevel.Info, [this.extractMessage(message), ...args]);
}
}
warn(message: string | Error, ...args: any[]): void {
if (this.checkLogLevel(LogLevel.Warning)) {
if (this.canLog(LogLevel.Warning)) {
this.adapter.log(LogLevel.Warning, [this.extractMessage(message), ...args]);
}
}
error(message: string | Error, ...args: any[]): void {
if (this.checkLogLevel(LogLevel.Error)) {
if (this.canLog(LogLevel.Error)) {
this.adapter.log(LogLevel.Error, [this.extractMessage(message), ...args]);
}
}
@@ -491,7 +497,7 @@ export class AdapterLogger extends AbstractLogger implements ILogger {
return msg;
}
return toErrorMessage(msg, this.checkLogLevel(LogLevel.Trace));
return toErrorMessage(msg, this.canLog(LogLevel.Trace));
}
flush(): void {
+8 -2
View File
@@ -111,9 +111,9 @@ export class SpdLogLogger extends AbstractMessageLogger implements ILogger {
override flush(): void {
if (this._logger) {
this._logger.flush();
this.flushLogger();
} else {
this._loggerCreationPromise.then(() => this.flush());
this._loggerCreationPromise.then(() => this.flushLogger());
}
}
@@ -126,6 +126,12 @@ export class SpdLogLogger extends AbstractMessageLogger implements ILogger {
super.dispose();
}
private flushLogger(): void {
if (this._logger) {
this._logger.flush();
}
}
private disposeLogger(): void {
if (this._logger) {
this._logger.drop();
@@ -21,31 +21,31 @@ class TestTelemetryLogger extends AbstractLogger implements ILogger {
}
trace(message: string, ...args: any[]): void {
if (this.checkLogLevel(LogLevel.Trace)) {
if (this.canLog(LogLevel.Trace)) {
this.logs.push(message + JSON.stringify(args));
}
}
debug(message: string, ...args: any[]): void {
if (this.checkLogLevel(LogLevel.Debug)) {
if (this.canLog(LogLevel.Debug)) {
this.logs.push(message);
}
}
info(message: string, ...args: any[]): void {
if (this.checkLogLevel(LogLevel.Info)) {
if (this.canLog(LogLevel.Info)) {
this.logs.push(message);
}
}
warn(message: string | Error, ...args: any[]): void {
if (this.checkLogLevel(LogLevel.Warning)) {
if (this.canLog(LogLevel.Warning)) {
this.logs.push(message.toString());
}
}
error(message: string, ...args: any[]): void {
if (this.checkLogLevel(LogLevel.Error)) {
if (this.canLog(LogLevel.Error)) {
this.logs.push(message);
}
}
@@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { asCSSPropertyValue, asCSSUrl } from '../../../base/browser/cssValue.js';
import * as css from '../../../base/browser/cssValue.js';
import { Emitter, Event } from '../../../base/common/event.js';
import { DisposableStore, IDisposable } from '../../../base/common/lifecycle.js';
import { ThemeIcon } from '../../../base/common/themables.js';
@@ -11,7 +11,7 @@ import { getIconRegistry, IconContribution, IconFontDefinition } from '../common
import { IProductIconTheme, IThemeService } from '../common/themeService.js';
export interface IIconsStyleSheet extends IDisposable {
getCSS(): string;
getCSS(): css.CssFragment;
readonly onDidChange: Event<void>;
}
@@ -28,12 +28,12 @@ export function getIconsStyleSheet(themeService: IThemeService | undefined): IIc
return {
dispose: () => disposable.dispose(),
onDidChange: onDidChangeEmmiter.event,
getCSS() {
getCSS(): css.CssFragment {
const productIconTheme = themeService ? themeService.getProductIconTheme() : new UnthemedProductIconTheme();
const usedFontIds: { [id: string]: IconFontDefinition } = {};
const rules: string[] = [];
const rootAttribs: string[] = [];
const rules = new css.Builder();
const rootAttribs = new css.Builder();
for (const contribution of iconRegistry.getIcons()) {
const definition = productIconTheme.getIcon(contribution);
if (!definition) {
@@ -41,30 +41,34 @@ export function getIconsStyleSheet(themeService: IThemeService | undefined): IIc
}
const fontContribution = definition.font;
const fontFamilyVar = `--vscode-icon-${contribution.id}-font-family`;
const contentVar = `--vscode-icon-${contribution.id}-content`;
const fontFamilyVar = css.inline`--vscode-icon-${css.className(contribution.id)}-font-family`;
const contentVar = css.inline`--vscode-icon-${css.className(contribution.id)}-content`;
if (fontContribution) {
usedFontIds[fontContribution.id] = fontContribution.definition;
rootAttribs.push(
`${fontFamilyVar}: ${asCSSPropertyValue(fontContribution.id)};`,
`${contentVar}: '${definition.fontCharacter}';`,
css.inline`${fontFamilyVar}: ${css.stringValue(fontContribution.id)};`,
css.inline`${contentVar}: ${css.stringValue(definition.fontCharacter)};`,
);
rules.push(`.codicon-${contribution.id}:before { content: '${definition.fontCharacter}'; font-family: ${asCSSPropertyValue(fontContribution.id)}; }`);
rules.push(css.inline`.codicon-${css.className(contribution.id)}:before { content: ${css.stringValue(definition.fontCharacter)}; font-family: ${css.stringValue(fontContribution.id)}; }`);
} else {
rootAttribs.push(`${contentVar}: '${definition.fontCharacter}'; ${fontFamilyVar}: 'codicon';`);
rules.push(`.codicon-${contribution.id}:before { content: '${definition.fontCharacter}'; }`);
rootAttribs.push(css.inline`${contentVar}: ${css.stringValue(definition.fontCharacter)}; ${fontFamilyVar}: 'codicon';`);
rules.push(css.inline`.codicon-${css.className(contribution.id)}:before { content: ${css.stringValue(definition.fontCharacter)}; }`);
}
}
for (const id in usedFontIds) {
const definition = usedFontIds[id];
const fontWeight = definition.weight ? `font-weight: ${definition.weight};` : '';
const fontStyle = definition.style ? `font-style: ${definition.style};` : '';
const src = definition.src.map(l => `${asCSSUrl(l.location)} format('${l.format}')`).join(', ');
rules.push(`@font-face { src: ${src}; font-family: ${asCSSPropertyValue(id)};${fontWeight}${fontStyle} font-display: block; }`);
const fontWeight = definition.weight ? css.inline`font-weight: ${css.value(definition.weight)};` : css.inline``;
const fontStyle = definition.style ? css.inline`font-style: ${css.value(definition.style)};` : css.inline``;
const src = new css.Builder();
for (const l of definition.src) {
src.push(css.inline`${css.asCSSUrl(l.location)} format(${css.stringValue(l.format)})`);
}
rules.push(css.inline`@font-face { src: ${src.join(', ')}; font-family: ${css.stringValue(id)};${fontWeight}${fontStyle} font-display: block; }`);
}
rules.push(`:root { ${rootAttribs.join(' ')} }`);
rules.push(css.inline`:root { ${rootAttribs.join(' ')} }`);
return rules.join('\n');
}
+5 -5
View File
@@ -276,7 +276,7 @@ class ServerLogger extends AbstractLogger {
}
trace(message: string, ...args: any[]): void {
if (this.checkLogLevel(LogLevel.Trace)) {
if (this.canLog(LogLevel.Trace)) {
if (this.useColors) {
console.log(`\x1b[90m[${now()}]\x1b[0m`, message, ...args);
} else {
@@ -286,7 +286,7 @@ class ServerLogger extends AbstractLogger {
}
debug(message: string, ...args: any[]): void {
if (this.checkLogLevel(LogLevel.Debug)) {
if (this.canLog(LogLevel.Debug)) {
if (this.useColors) {
console.log(`\x1b[90m[${now()}]\x1b[0m`, message, ...args);
} else {
@@ -296,7 +296,7 @@ class ServerLogger extends AbstractLogger {
}
info(message: string, ...args: any[]): void {
if (this.checkLogLevel(LogLevel.Info)) {
if (this.canLog(LogLevel.Info)) {
if (this.useColors) {
console.log(`\x1b[90m[${now()}]\x1b[0m`, message, ...args);
} else {
@@ -306,7 +306,7 @@ class ServerLogger extends AbstractLogger {
}
warn(message: string | Error, ...args: any[]): void {
if (this.checkLogLevel(LogLevel.Warning)) {
if (this.canLog(LogLevel.Warning)) {
if (this.useColors) {
console.warn(`\x1b[93m[${now()}]\x1b[0m`, message, ...args);
} else {
@@ -316,7 +316,7 @@ class ServerLogger extends AbstractLogger {
}
error(message: string, ...args: any[]): void {
if (this.checkLogLevel(LogLevel.Error)) {
if (this.canLog(LogLevel.Error)) {
if (this.useColors) {
console.error(`\x1b[91m[${now()}]\x1b[0m`, message, ...args);
} else {
@@ -18,7 +18,6 @@ import { ServiceCollection } from '../../../platform/instantiation/common/servic
import { ILabelService } from '../../../platform/label/common/label.js';
import { AbstractMessageLogger, ILogger, LogLevel } from '../../../platform/log/common/log.js';
import { IOpenerService } from '../../../platform/opener/common/opener.js';
import { IUriIdentityService } from '../../../platform/uriIdentity/common/uriIdentity.js';
import { IOpenWindowOptions, IWindowOpenable } from '../../../platform/window/common/window.js';
import { IWorkbenchEnvironmentService } from '../../services/environment/common/environmentService.js';
import { IExtensionManagementServerService } from '../../services/extensionManagement/common/extensionManagement.js';
@@ -104,12 +103,11 @@ class RemoteExtensionManagementCLI extends ExtensionManagementCLI {
logger: ILogger,
@IExtensionManagementService extensionManagementService: IExtensionManagementService,
@IExtensionGalleryService extensionGalleryService: IExtensionGalleryService,
@IUriIdentityService uriIdentityService: IUriIdentityService,
@ILabelService labelService: ILabelService,
@IWorkbenchEnvironmentService envService: IWorkbenchEnvironmentService,
@IExtensionManifestPropertiesService private readonly _extensionManifestPropertiesService: IExtensionManifestPropertiesService,
) {
super(logger, extensionManagementService, extensionGalleryService, uriIdentityService);
super(logger, extensionManagementService, extensionGalleryService);
const remoteAuthority = envService.remoteAuthority;
this._location = remoteAuthority ? labelService.getHostLabel(Schemas.vscodeRemote, remoteAuthority) : undefined;
@@ -25,7 +25,7 @@ import { IChatWidgetService } from '../../contrib/chat/browser/chat.js';
import { ChatInputPart } from '../../contrib/chat/browser/chatInputPart.js';
import { AddDynamicVariableAction, IAddDynamicVariableContext } from '../../contrib/chat/browser/contrib/chatDynamicVariables.js';
import { ChatAgentLocation, IChatAgentHistoryEntry, IChatAgentImplementation, IChatAgentRequest, IChatAgentService } from '../../contrib/chat/common/chatAgents.js';
import { IChatEditingService } from '../../contrib/chat/common/chatEditingService.js';
import { IChatEditingService, IChatRelatedFileProviderMetadata } from '../../contrib/chat/common/chatEditingService.js';
import { ChatRequestAgentPart } from '../../contrib/chat/common/chatParserTypes.js';
import { ChatRequestParser } from '../../contrib/chat/common/chatRequestParser.js';
import { IChatContentInlineReference, IChatContentReference, IChatFollowup, IChatProgress, IChatService, IChatTask, IChatWarningMessage } from '../../contrib/chat/common/chatService.js';
@@ -349,8 +349,9 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA
this._chatParticipantDetectionProviders.deleteAndDispose(handle);
}
$registerRelatedFilesProvider(handle: number): void {
$registerRelatedFilesProvider(handle: number, metadata: IChatRelatedFileProviderMetadata): void {
this._chatRelatedFilesProviders.set(handle, this._chatEditingService.registerRelatedFilesProvider(handle, {
description: metadata.description,
provideRelatedFiles: async (request, token) => {
return (await this._proxy.$provideRelatedFiles(handle, request, token))?.map((v) => ({ uri: URI.from(v.uri), description: v.description })) ?? [];
}
@@ -132,7 +132,8 @@ const defaultConfigurationExtPoint = ExtensionsRegistry.registerExtensionPoint<I
extensionPoint: 'configurationDefaults',
jsonSchema: {
$ref: configurationDefaultsSchemaId,
}
},
canHandleResolver: true
});
defaultConfigurationExtPoint.setHandler((extensions, { added, removed }) => {
@@ -195,7 +196,8 @@ const configurationExtPoint = ExtensionsRegistry.registerExtensionPoint<IConfigu
items: configurationEntrySchema
}
]
}
},
canHandleResolver: true
});
const extensionConfigurations: ExtensionIdentifierMap<IConfigurationNode[]> = new ExtensionIdentifierMap<IConfigurationNode[]>();
@@ -52,7 +52,7 @@ import { IRevealOptions, ITreeItem, IViewBadge } from '../../common/views.js';
import { CallHierarchyItem } from '../../contrib/callHierarchy/common/callHierarchy.js';
import { ChatAgentLocation, IChatAgentMetadata, IChatAgentRequest, IChatAgentResult, IChatWelcomeMessageContent } from '../../contrib/chat/common/chatAgents.js';
import { ICodeMapperRequest, ICodeMapperResult } from '../../contrib/chat/common/chatCodeMapperService.js';
import { IChatRelatedFile, IChatRequestDraft } from '../../contrib/chat/common/chatEditingService.js';
import { IChatRelatedFile, IChatRelatedFileProviderMetadata as IChatRelatedFilesProviderMetadata, IChatRequestDraft } from '../../contrib/chat/common/chatEditingService.js';
import { IChatProgressHistoryResponseContent } from '../../contrib/chat/common/chatModel.js';
import { IChatContentInlineReference, IChatFollowup, IChatProgress, IChatResponseErrorDetails, IChatTask, IChatTaskDto, IChatUserActionEvent, IChatVoteAction } from '../../contrib/chat/common/chatService.js';
import { IChatRequestVariableValue, IChatVariableData, IChatVariableResolverProgress } from '../../contrib/chat/common/chatVariables.js';
@@ -1275,7 +1275,7 @@ export interface MainThreadChatAgentsShape2 extends IDisposable {
$registerAgent(handle: number, extension: ExtensionIdentifier, id: string, metadata: IExtensionChatAgentMetadata, dynamicProps: IDynamicChatAgentProps | undefined): void;
$registerChatParticipantDetectionProvider(handle: number): void;
$unregisterChatParticipantDetectionProvider(handle: number): void;
$registerRelatedFilesProvider(handle: number): void;
$registerRelatedFilesProvider(handle: number, metadata: IChatRelatedFilesProviderMetadata): void;
$unregisterRelatedFilesProvider(handle: number): void;
$registerAgentCompletionsProvider(handle: number, id: string, triggerCharacters: string[]): void;
$unregisterAgentCompletionsProvider(handle: number, id: string): void;
@@ -369,7 +369,7 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS
registerRelatedFilesProvider(extension: IExtensionDescription, provider: vscode.ChatRelatedFilesProvider, metadata: vscode.ChatRelatedFilesProviderMetadata): vscode.Disposable {
const handle = ExtHostChatAgents2._relatedFilesProviderIdPool++;
this._relatedFilesProviders.set(handle, new ExtHostRelatedFilesProvider(extension, provider));
this._proxy.$registerRelatedFilesProvider(handle);
this._proxy.$registerRelatedFilesProvider(handle, metadata);
return toDisposable(() => {
this._relatedFilesProviders.delete(handle);
this._proxy.$unregisterRelatedFilesProvider(handle);
+10 -10
View File
@@ -283,6 +283,7 @@ export function createExtHostQuickOpen(mainContext: IMainContext, workspace: IEx
private _busy = false;
private _ignoreFocusOut = true;
private _value = '';
private _valueSelection: readonly [number, number] | undefined = undefined;
private _placeholder: string | undefined;
private _buttons: QuickInputButton[] = [];
private _handlesToButtons = new Map<number, QuickInputButton>();
@@ -367,6 +368,15 @@ export function createExtHostQuickOpen(mainContext: IMainContext, workspace: IEx
this.update({ value });
}
get valueSelection() {
return this._valueSelection;
}
set valueSelection(valueSelection: readonly [number, number] | undefined) {
this._valueSelection = valueSelection;
this.update({ valueSelection });
}
get placeholder() {
return this._placeholder;
}
@@ -713,7 +723,6 @@ export function createExtHostQuickOpen(mainContext: IMainContext, workspace: IEx
private _password = false;
private _prompt: string | undefined;
private _valueSelection: readonly [number, number] | undefined;
private _validationMessage: string | InputBoxValidationMessage | undefined;
constructor(extension: IExtensionDescription, onDispose: () => void) {
@@ -739,15 +748,6 @@ export function createExtHostQuickOpen(mainContext: IMainContext, workspace: IEx
this.update({ prompt });
}
get valueSelection() {
return this._valueSelection;
}
set valueSelection(valueSelection: readonly [number, number] | undefined) {
this._valueSelection = valueSelection;
this.update({ valueSelection });
}
get validationMessage() {
return this._validationMessage;
}
@@ -708,6 +708,12 @@ const configuration: IConfigurationNode = {
'type': 'boolean',
'description': localize('accessibility.replEditor.readLastExecutedOutput', "Controls whether the output from an execution in the native REPL will be announced."),
'default': true,
},
'accessibility.replEditor.autoFocusReplExecution': {
type: 'string',
enum: ['none', 'input', 'lastExecution'],
default: 'lastExecution',
description: localize('replEditor.autoFocusAppendedCell', "Control whether focus should automatically be sent to the REPL when code is executed."),
}
}
};
@@ -8,6 +8,7 @@ import { Codicon } from '../../../../../base/common/codicons.js';
import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js';
import { Schemas } from '../../../../../base/common/network.js';
import { isElectron } from '../../../../../base/common/platform.js';
import { dirname } from '../../../../../base/common/resources.js';
import { compare } from '../../../../../base/common/strings.js';
import { ThemeIcon } from '../../../../../base/common/themables.js';
import { URI } from '../../../../../base/common/uri.js';
@@ -63,7 +64,7 @@ export function registerChatContextActions() {
/**
* We fill the quickpick with these types, and enable some quick access providers
*/
type IAttachmentQuickPickItem = ICommandVariableQuickPickItem | IQuickAccessQuickPickItem | IToolQuickPickItem | IImageQuickPickItem | IVariableQuickPickItem | IOpenEditorsQuickPickItem | ISearchResultsQuickPickItem | IScreenShotQuickPickItem;
type IAttachmentQuickPickItem = ICommandVariableQuickPickItem | IQuickAccessQuickPickItem | IToolQuickPickItem | IImageQuickPickItem | IVariableQuickPickItem | IOpenEditorsQuickPickItem | ISearchResultsQuickPickItem | IScreenShotQuickPickItem | IRelatedFilesQuickPickItem;
/**
* These are the types that we can get out of the quick pick
@@ -110,6 +111,19 @@ function isScreenshotQuickPickItem(obj: unknown): obj is IScreenShotQuickPickIte
&& (obj as IScreenShotQuickPickItem).kind === 'screenshot');
}
function isRelatedFileQuickPickItem(obj: unknown): obj is IRelatedFilesQuickPickItem {
return (
typeof obj === 'object'
&& (obj as IRelatedFilesQuickPickItem).kind === 'related-files'
);
}
interface IRelatedFilesQuickPickItem extends IQuickPickItem {
kind: 'related-files';
id: string;
label: string;
}
interface IImageQuickPickItem extends IQuickPickItem {
kind: 'image';
id: string;
@@ -384,7 +398,7 @@ export class AttachContextAction extends Action2 {
`:${item.range.startLineNumber}`);
}
private async _attachContext(widget: IChatWidget, commandService: ICommandService, clipboardService: IClipboardService, editorService: IEditorService, labelService: ILabelService, viewsService: IViewsService, chatEditingService: IChatEditingService | undefined, hostService: IHostService, isInBackground?: boolean, ...picks: IChatContextQuickPickItem[]) {
private async _attachContext(widget: IChatWidget, quickInputService: IQuickInputService, commandService: ICommandService, clipboardService: IClipboardService, editorService: IEditorService, labelService: ILabelService, viewsService: IViewsService, chatEditingService: IChatEditingService | undefined, hostService: IHostService, isInBackground?: boolean, ...picks: IChatContextQuickPickItem[]) {
const toAttach: IChatRequestVariableEntry[] = [];
for (const pick of picks) {
if (isISymbolQuickPickItem(pick) && pick.symbol) {
@@ -462,6 +476,37 @@ export class AttachContextAction extends Action2 {
});
}
}
} else if (isRelatedFileQuickPickItem(pick)) {
// Get all provider results and show them in a second tier picker
const chatSessionId = widget.viewModel?.sessionId;
if (!chatSessionId || !chatEditingService) {
continue;
}
const relatedFiles = await chatEditingService.getRelatedFiles(chatSessionId, widget.getInput(), CancellationToken.None);
if (!relatedFiles) {
continue;
}
const attachments = widget.attachmentModel.getAttachmentIDs();
const itemsPromise = chatEditingService.getRelatedFiles(chatSessionId, widget.getInput(), CancellationToken.None)
.then((files) => (files ?? []).reduce<((IQuickPickItem & { value: URI }) | IQuickPickSeparator)[]>((acc, cur) => {
acc.push({ type: 'separator', label: cur.group });
const workingSet = chatEditingService.currentEditingSessionObs.get()?.workingSet;
for (const file of cur.files) {
acc.push({
type: 'item',
label: labelService.getUriBasenameLabel(file.uri),
description: labelService.getUriLabel(dirname(file.uri), { relative: true }),
value: file.uri,
disabled: workingSet?.has(file.uri) || attachments.has(this._getFileContextId({ resource: file.uri })),
picked: true
});
}
return acc;
}, []));
const selectedFiles = await quickInputService.pick(itemsPromise, { placeHolder: localize('relatedFiles', 'Add related files to your working set'), canPickMany: true });
for (const file of selectedFiles ?? []) {
chatEditingService?.currentEditingSessionObs.get()?.addFileToWorkingSet(file.value);
}
} else if (isScreenshotQuickPickItem(pick)) {
const blob = await hostService.getScreenshot();
if (blob) {
@@ -654,6 +699,14 @@ export class AttachContextAction extends Action2 {
});
}
} else if (context.showFilesOnly) {
if (chatEditingService?.hasRelatedFilesProviders() && (widget.getInput() || chatEditingService.currentEditingSessionObs.get()?.workingSet.size)) {
quickPickItems.push({
kind: 'related-files',
id: 'related-files',
label: localize('chatContext.relatedFiles', 'Related Files'),
iconClass: ThemeIcon.asClassName(Codicon.sparkle),
});
}
if (editorService.editors.filter(e => e instanceof FileEditorInput || e instanceof DiffEditorInput || e instanceof UntitledTextEditorInput).length > 0) {
quickPickItems.push({
kind: 'open-editors',
@@ -698,7 +751,7 @@ export class AttachContextAction extends Action2 {
if (!clipboardService) {
return;
}
this._attachContext(widget, commandService, clipboardService, editorService, labelService, viewsService, chatEditingService, hostService, isBackgroundAccept, item);
this._attachContext(widget, quickInputService, commandService, clipboardService, editorService, labelService, viewsService, chatEditingService, hostService, isBackgroundAccept, item);
if (isQuickChat(widget)) {
quickChatService.open();
}
@@ -78,6 +78,7 @@ import { ILanguageModelIgnoredFilesService, LanguageModelIgnoredFilesService } f
import { ChatGettingStartedContribution } from './actions/chatGettingStarted.js';
import { Extensions, IConfigurationMigrationRegistry } from '../../../common/configuration.js';
import { ChatEditorOverlayController } from './chatEditorOverlay.js';
import { ChatRelatedFilesContribution } from './contrib/chatInputRelatedFilesContrib.js';
// Register configuration
const configurationRegistry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration);
@@ -312,6 +313,7 @@ registerWorkbenchContribution2(LanguageModelToolsExtensionPointHandler.ID, Langu
registerWorkbenchContribution2(ChatCompatibilityNotifier.ID, ChatCompatibilityNotifier, WorkbenchPhase.Eventually);
registerWorkbenchContribution2(ChatCommandCenterRendering.ID, ChatCommandCenterRendering, WorkbenchPhase.AfterRestored);
registerWorkbenchContribution2(ChatImplicitContextContribution.ID, ChatImplicitContextContribution, WorkbenchPhase.Eventually);
registerWorkbenchContribution2(ChatRelatedFilesContribution.ID, ChatRelatedFilesContribution, WorkbenchPhase.Eventually);
registerWorkbenchContribution2(ChatEditorSaving.ID, ChatEditorSaving, WorkbenchPhase.AfterRestored);
registerWorkbenchContribution2(ChatViewsWelcomeHandler.ID, ChatViewsWelcomeHandler, WorkbenchPhase.BlockStartup);
registerWorkbenchContribution2(ChatGettingStartedContribution.ID, ChatGettingStartedContribution, WorkbenchPhase.Eventually);
@@ -9,13 +9,17 @@ import { Emitter } from '../../../../../base/common/event.js';
import { Disposable, DisposableStore } from '../../../../../base/common/lifecycle.js';
import { basename, dirname } from '../../../../../base/common/path.js';
import { URI } from '../../../../../base/common/uri.js';
import { Range } from '../../../../../editor/common/core/range.js';
import { IRange, Range } from '../../../../../editor/common/core/range.js';
import { localize } from '../../../../../nls.js';
import { ICommandService } from '../../../../../platform/commands/common/commands.js';
import { ITextEditorOptions } from '../../../../../platform/editor/common/editor.js';
import { FileKind, IFileService } from '../../../../../platform/files/common/files.js';
import { IHoverService } from '../../../../../platform/hover/browser/hover.js';
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
import { IOpenerService } from '../../../../../platform/opener/common/opener.js';
import { IOpenerService, OpenInternalOptions } from '../../../../../platform/opener/common/opener.js';
import { FolderThemeIcon, IThemeService } from '../../../../../platform/theme/common/themeService.js';
import { ResourceLabels } from '../../../../browser/labels.js';
import { revealInsideBarCommand } from '../../../files/browser/fileActions.contribution.js';
import { IChatRequestVariableEntry } from '../../common/chatModel.js';
import { ChatResponseReferencePartStatusKind, IChatContentReference } from '../../common/chatService.js';
@@ -33,7 +37,9 @@ export class ChatAttachmentsContentPart extends Disposable {
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IOpenerService private readonly openerService: IOpenerService,
@IHoverService private readonly hoverService: IHoverService,
@IFileService private readonly fileService: IFileService
@IFileService private readonly fileService: IFileService,
@ICommandService private readonly commandService: ICommandService,
@IThemeService private readonly themeService: IThemeService
) {
super();
@@ -47,9 +53,9 @@ export class ChatAttachmentsContentPart extends Disposable {
const hoverDelegate = this.attachedContextDisposables.add(createInstantHoverDelegate());
this.variables.forEach(async (attachment) => {
const file = URI.isUri(attachment.value) ? attachment.value : attachment.value && typeof attachment.value === 'object' && 'uri' in attachment.value && URI.isUri(attachment.value.uri) ? attachment.value.uri : undefined;
const resource = URI.isUri(attachment.value) ? attachment.value : attachment.value && typeof attachment.value === 'object' && 'uri' in attachment.value && URI.isUri(attachment.value.uri) ? attachment.value.uri : undefined;
const range = attachment.value && typeof attachment.value === 'object' && 'range' in attachment.value && Range.isIRange(attachment.value.range) ? attachment.value.range : undefined;
if (file && attachment.isFile && this.workingSet.find(entry => entry.toString() === file.toString())) {
if (resource && attachment.isFile && this.workingSet.find(entry => entry.toString() === resource.toString())) {
// Don't render attachment if it's in the working set
return;
}
@@ -63,9 +69,9 @@ export class ChatAttachmentsContentPart extends Disposable {
let ariaLabel: string | undefined;
if (file && attachment.isFile) {
const fileBasename = basename(file.path);
const fileDirname = dirname(file.path);
if (resource && (attachment.isFile || attachment.isDirectory)) {
const fileBasename = basename(resource.path);
const fileDirname = dirname(resource.path);
const friendlyName = `${fileBasename} ${fileDirname}`;
if (isAttachmentOmitted) {
@@ -76,12 +82,20 @@ export class ChatAttachmentsContentPart extends Disposable {
ariaLabel = range ? localize('chat.fileAttachmentWithRange3', "Attached: {0}, line {1} to line {2}.", friendlyName, range.startLineNumber, range.endLineNumber) : localize('chat.fileAttachment3', "Attached: {0}.", friendlyName);
}
label.setFile(file, {
fileKind: FileKind.FILE,
const fileOptions = {
hidePath: true,
range,
title: correspondingContentReference?.options?.status?.description
};
label.setFile(resource, attachment.isFile ? {
...fileOptions,
fileKind: FileKind.FILE,
range,
} : {
...fileOptions,
fileKind: FileKind.FOLDER,
icon: !this.themeService.getFileIconTheme().hasFolderIcons ? FolderThemeIcon : undefined
});
} else if (attachment.isImage) {
ariaLabel = localize('chat.imageAttachment', "Attached image, {0}", attachment.name);
@@ -134,19 +148,16 @@ export class ChatAttachmentsContentPart extends Disposable {
}
}
if (file) {
if (resource) {
widget.style.cursor = 'pointer';
if (!this.attachedContextDisposables.isDisposed) {
this.attachedContextDisposables.add(dom.addDisposableListener(widget, dom.EventType.CLICK, async (e: MouseEvent) => {
dom.EventHelper.stop(e, true);
this.openerService.open(
file,
{
fromUserGesture: true,
editorOptions: {
selection: range
} as any
});
if (attachment.isDirectory) {
this.openResource(resource, true);
} else {
this.openResource(resource, false, range);
}
}));
}
}
@@ -156,6 +167,24 @@ export class ChatAttachmentsContentPart extends Disposable {
});
}
private openResource(resource: URI, isDirectory: true): void;
private openResource(resource: URI, isDirectory: false, range: IRange | undefined): void;
private openResource(resource: URI, isDirectory?: boolean, range?: IRange): void {
if (isDirectory) {
// Reveal Directory in explorer
this.commandService.executeCommand(revealInsideBarCommand.id, resource);
return;
}
// Open file in editor
const openTextEditorOptions: ITextEditorOptions | undefined = range ? { selection: range } : undefined;
const options: OpenInternalOptions = {
fromUserGesture: true,
editorOptions: openTextEditorOptions,
};
this.openerService.open(resource, options);
}
// Helper function to create and replace image
private async createImageElements(buffer: ArrayBuffer | Uint8Array, widget: HTMLElement, hoverElement: HTMLElement) {
const blob = new Blob([buffer], { type: 'image/png' });
@@ -48,6 +48,7 @@ const $ = dom.$;
export interface IChatReferenceListItem extends IChatContentReference {
title?: string;
description?: string;
state?: WorkingSetEntryState;
}
@@ -382,12 +383,12 @@ class CollapsibleListRenderer implements IListRenderer<IChatCollapsibleListItem,
} else if (matchesSomeScheme(uri, Schemas.mailto, Schemas.http, Schemas.https)) {
templateData.label.setResource({ resource: uri, name: uri.toString() }, { icon: icon ?? Codicon.globe, title: data.options?.status?.description ?? data.title ?? uri.toString() });
} else {
if (data.state === WorkingSetEntryState.Transient) {
if (data.state === WorkingSetEntryState.Transient || data.state === WorkingSetEntryState.Suggested) {
templateData.label.setResource(
{
resource: uri,
name: basenameOrAuthority(uri),
description: localize('chat.openEditor', 'Open Editor'),
description: data.description ?? localize('chat.openEditor', 'Open Editor'),
range: 'range' in reference ? reference.range : undefined,
}, { icon, title: data.options?.status?.description ?? data.title });
} else {
@@ -8,10 +8,12 @@ import { $, DragAndDropObserver } from '../../../../base/browser/dom.js';
import { renderLabelWithIcons } from '../../../../base/browser/ui/iconLabel/iconLabels.js';
import { coalesce } from '../../../../base/common/arrays.js';
import { Codicon } from '../../../../base/common/codicons.js';
import { Mimes } from '../../../../base/common/mime.js';
import { basename } from '../../../../base/common/resources.js';
import { URI } from '../../../../base/common/uri.js';
import { localize } from '../../../../nls.js';
import { containsDragType, extractEditorsDropData, IDraggedResourceEditorInput } from '../../../../platform/dnd/browser/dnd.js';
import { IFileService } from '../../../../platform/files/common/files.js';
import { IThemeService, Themable } from '../../../../platform/theme/common/themeService.js';
import { EditorInput } from '../../../common/editor/editorInput.js';
import { IExtensionService, isProposedApiEnabled } from '../../../services/extensions/common/extensions.js';
@@ -20,7 +22,9 @@ import { ChatInputPart } from './chatInputPart.js';
import { IChatWidgetStyles } from './chatWidget.js';
enum ChatDragAndDropType {
FILE,
FILE_INTERNAL,
FILE_EXTERNAL,
FOLDER,
IMAGE
}
@@ -35,7 +39,8 @@ export class ChatDragAndDrop extends Themable {
private readonly inputPart: ChatInputPart,
private readonly styles: IChatWidgetStyles,
@IThemeService themeService: IThemeService,
@IExtensionService private readonly extensionService: IExtensionService
@IExtensionService private readonly extensionService: IExtensionService,
@IFileService private readonly fileService: IFileService
) {
super(themeService);
@@ -85,8 +90,11 @@ export class ChatDragAndDrop extends Themable {
private onDrop(e: DragEvent): void {
this.updateDropFeedback(e, undefined);
this.drop(e);
}
const contexts = this.getAttachContext(e);
private async drop(e: DragEvent): Promise<void> {
const contexts = await this.getAttachContext(e);
if (contexts.length === 0) {
return;
}
@@ -94,17 +102,7 @@ export class ChatDragAndDrop extends Themable {
e.stopPropagation();
e.preventDefault();
// Make sure to attach only new contexts
const currentContextIds = this.inputPart.attachmentModel.getAttachmentIDs();
const filteredContext = [];
for (const context of contexts) {
if (!currentContextIds.has(context.id)) {
currentContextIds.add(context.id);
filteredContext.push(context);
}
}
this.inputPart.attachmentModel.addContext(...filteredContext);
this.inputPart.attachmentModel.addContext(...contexts);
}
private updateDropFeedback(e: DragEvent, dropType: ChatDragAndDropType | undefined): void {
@@ -116,6 +114,36 @@ export class ChatDragAndDrop extends Themable {
this.setOverlay(dropType);
}
private guessDropType(e: DragEvent): ChatDragAndDropType | undefined {
// 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, DataTransfers.FILES)) {
return ChatDragAndDropType.FILE_EXTERNAL;
} else if (containsDragType(e, DataTransfers.INTERNAL_URI_LIST)) {
return ChatDragAndDropType.FILE_INTERNAL;
} else if (containsDragType(e, Mimes.uriList)) {
return ChatDragAndDropType.FOLDER;
}
return undefined;
}
private isDragEventSupported(e: DragEvent): boolean {
// if guessed drop type is undefined, it means the drop is not supported
const dropType = this.guessDropType(e);
return dropType !== undefined;
}
private getDropTypeName(type: ChatDragAndDropType): string {
switch (type) {
case ChatDragAndDropType.FILE_INTERNAL: return localize('file', 'File');
case ChatDragAndDropType.FILE_EXTERNAL: return localize('file', 'File');
case ChatDragAndDropType.FOLDER: return localize('folder', 'Folder');
case ChatDragAndDropType.IMAGE: return localize('image', 'Image');
}
}
private isImageDnd(e: DragEvent): boolean {
// Image detection should not have false positives, only false negatives are allowed
if (containsDragType(e, 'image')) {
@@ -139,42 +167,18 @@ export class ChatDragAndDrop extends Themable {
return false;
}
private guessDropType(e: DragEvent): ChatDragAndDropType | undefined {
// 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, DataTransfers.FILES, DataTransfers.INTERNAL_URI_LIST)) {
return ChatDragAndDropType.FILE;
}
return undefined;
}
private isDragEventSupported(e: DragEvent): boolean {
// if guessed drop type is undefined, it means the drop is not supported
const dropType = this.guessDropType(e);
return dropType !== undefined;
}
private getDropTypeName(type: ChatDragAndDropType): string {
switch (type) {
case ChatDragAndDropType.FILE: return localize('file', 'File');
case ChatDragAndDropType.IMAGE: return localize('image', 'Image');
}
}
private getAttachContext(e: DragEvent): IChatRequestVariableEntry[] {
private async getAttachContext(e: DragEvent): Promise<IChatRequestVariableEntry[]> {
if (!this.isDragEventSupported(e)) {
return [];
}
const data = extractEditorsDropData(e);
return coalesce(data.map(editorInput => {
return coalesce(await Promise.all(data.map(editorInput => {
return this.resolveAttachContext(editorInput);
}));
})));
}
private resolveAttachContext(editorInput: IDraggedResourceEditorInput): IChatRequestVariableEntry | undefined {
private async resolveAttachContext(editorInput: IDraggedResourceEditorInput): Promise<IChatRequestVariableEntry | undefined> {
// Image
const imageContext = getImageAttachContext(editorInput);
if (imageContext) {
@@ -182,7 +186,26 @@ export class ChatDragAndDrop extends Themable {
}
// File
return getEditorAttachContext(editorInput);
return await this.getEditorAttachContext(editorInput);
}
private async getEditorAttachContext(editor: EditorInput | IDraggedResourceEditorInput): Promise<IChatRequestVariableEntry | undefined> {
if (!editor.resource) {
return undefined;
}
let stat;
try {
stat = await this.fileService.stat(editor.resource);
} catch {
return undefined;
}
if (!stat.isDirectory && !stat.isFile) {
return undefined;
}
return getResourceAttachContext(editor.resource, stat.isDirectory);
}
private setOverlay(type: ChatDragAndDropType | undefined): void {
@@ -217,20 +240,14 @@ export class ChatDragAndDrop extends Themable {
}
}
function getEditorAttachContext(editor: EditorInput | IDraggedResourceEditorInput): IChatRequestVariableEntry | undefined {
if (!editor.resource) {
return undefined;
}
return getFileAttachContext(editor.resource);
}
function getFileAttachContext(resource: URI): IChatRequestVariableEntry | undefined {
function getResourceAttachContext(resource: URI, isDirectory: boolean): IChatRequestVariableEntry | undefined {
return {
value: resource,
id: resource.toString(),
name: basename(resource),
isFile: true,
isFile: !isDirectory,
isDirectory,
isDynamic: true
};
}
@@ -65,7 +65,7 @@ registerAction2(class AddFileToWorkingSet extends WorkingSetAction {
icon: Codicon.plus,
menu: [{
id: MenuId.ChatEditingWidgetModifiedFilesToolbar,
when: ContextKeyExpr.equals(chatEditingWidgetFileStateContextKey.key, WorkingSetEntryState.Transient),
when: ContextKeyExpr.or(ContextKeyExpr.equals(chatEditingWidgetFileStateContextKey.key, WorkingSetEntryState.Transient), ContextKeyExpr.equals(chatEditingWidgetFileStateContextKey.key, WorkingSetEntryState.Suggested)),
order: 0,
group: 'navigation'
}],
@@ -87,7 +87,7 @@ 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.Transient)),
when: ContextKeyExpr.or(ContextKeyExpr.equals(chatEditingWidgetFileStateContextKey.key, WorkingSetEntryState.Attached), ContextKeyExpr.equals(chatEditingWidgetFileStateContextKey.key, WorkingSetEntryState.Suggested), ContextKeyExpr.equals(chatEditingWidgetFileStateContextKey.key, WorkingSetEntryState.Transient)),
order: 0,
group: 'navigation'
}],
@@ -3,8 +3,8 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { compareBy, delta } from '../../../../../base/common/arrays.js';
import { AsyncIterableSource, raceTimeout } from '../../../../../base/common/async.js';
import { coalesce, compareBy, delta } from '../../../../../base/common/arrays.js';
import { AsyncIterableSource } from '../../../../../base/common/async.js';
import { CancellationToken, CancellationTokenSource } from '../../../../../base/common/cancellation.js';
import { Codicon } from '../../../../../base/common/codicons.js';
import { BugIndicatingError } from '../../../../../base/common/errors.js';
@@ -393,6 +393,10 @@ export class ChatEditingService extends Disposable implements IChatEditingServic
return editors;
}
hasRelatedFilesProviders(): boolean {
return this._chatRelatedFilesProviders.size > 0;
}
registerRelatedFilesProvider(handle: number, provider: IChatRelatedFilesProvider): IDisposable {
this._chatRelatedFilesProviders.set(handle, provider);
return toDisposable(() => {
@@ -400,28 +404,34 @@ export class ChatEditingService extends Disposable implements IChatEditingServic
});
}
async getRelatedFiles(chatSessionId: string, prompt: string, token: CancellationToken): Promise<readonly IChatRelatedFile[] | undefined> {
async getRelatedFiles(chatSessionId: string, prompt: string, token: CancellationToken): Promise<{ group: string; files: IChatRelatedFile[] }[] | undefined> {
const currentSession = this._currentSessionObs.get();
if (!currentSession || chatSessionId !== currentSession.chatSessionId) {
return undefined;
}
const currentWorkingSet = [...currentSession.workingSet.keys()];
const userAddedWorkingSetEntries: URI[] = [];
for (const entry of currentSession.workingSet) {
// Don't incorporate suggested files into the related files request
// but do consider transient entries like open editors
if (entry[1].state !== WorkingSetEntryState.Suggested) {
userAddedWorkingSetEntries.push(entry[0]);
}
}
const providers = Array.from(this._chatRelatedFilesProviders.values());
const result = await Promise.all(providers.map(async provider => {
try {
return await raceTimeout(provider.provideRelatedFiles({ prompt, files: currentWorkingSet }, token), 2000);
const relatedFiles = await provider.provideRelatedFiles({ prompt, files: userAddedWorkingSetEntries }, token);
if (relatedFiles?.length) {
return { group: provider.description, files: relatedFiles };
}
return undefined;
} catch (e) {
return undefined;
}
}));
return result.reduce<IChatRelatedFile[]>((acc, cur) => {
if (cur) {
acc.push(...cur);
}
return acc;
}, []);
return coalesce(result);
}
}
@@ -30,7 +30,7 @@ import { IEditorService } from '../../../../services/editor/common/editorService
import { MultiDiffEditor } from '../../../multiDiffEditor/browser/multiDiffEditor.js';
import { MultiDiffEditorInput } from '../../../multiDiffEditor/browser/multiDiffEditorInput.js';
import { ChatAgentLocation, IChatAgentService } from '../../common/chatAgents.js';
import { ChatEditingSessionState, ChatEditKind, IChatEditingSession, WorkingSetEntryState } from '../../common/chatEditingService.js';
import { ChatEditingSessionChangeType, ChatEditingSessionState, ChatEditKind, IChatEditingSession, WorkingSetDisplayMetadata, WorkingSetEntryState } from '../../common/chatEditingService.js';
import { IChatResponseModel } from '../../common/chatModel.js';
import { IChatWidgetService } from '../chat.js';
import { ChatEditingMultiDiffSourceResolver } from './chatEditingService.js';
@@ -59,14 +59,14 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio
}
private readonly _sequencer = new Sequencer();
private _workingSet = new ResourceMap<WorkingSetEntryState>();
private _workingSet = new ResourceMap<WorkingSetDisplayMetadata>();
get workingSet() {
this._assertNotDisposed();
// Return here a reunion between the AI modified entries and the user built working set
const result = new ResourceMap<WorkingSetEntryState>(this._workingSet);
const result = new ResourceMap<WorkingSetDisplayMetadata>(this._workingSet);
for (const entry of this._entriesObs.get()) {
result.set(entry.modifiedURI, entry.state.get());
result.set(entry.modifiedURI, { state: entry.state.get() });
}
return result;
@@ -101,7 +101,7 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio
return linearHistory.slice(linearHistoryIndex).map(s => s.requestId).filter((r): r is string => !!r);
});
private readonly _onDidChange = new Emitter<void>();
private readonly _onDidChange = new Emitter<ChatEditingSessionChangeType>();
get onDidChange() {
this._assertNotDisposed();
return this._onDidChange.event;
@@ -155,7 +155,7 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio
entries.forEach(entry => {
entry.state.read(reader);
});
this._onDidChange.fire();
this._onDidChange.fire(ChatEditingSessionChangeType.WorkingSet);
}));
}
@@ -164,7 +164,7 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio
const existingTransientEntries = new ResourceSet();
for (const file of this._workingSet.keys()) {
if (this._workingSet.get(file) === WorkingSetEntryState.Transient) {
if (this._workingSet.get(file)?.state === WorkingSetEntryState.Transient) {
existingTransientEntries.add(file);
}
}
@@ -199,12 +199,12 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio
}
for (const entry of activeEditors) {
this._workingSet.set(entry, WorkingSetEntryState.Transient);
this._workingSet.set(entry, { state: WorkingSetEntryState.Transient, description: localize('chatEditing.transient', "Open Editor") });
didChange = true;
}
if (didChange) {
this._onDidChange.fire();
this._onDidChange.fire(ChatEditingSessionChangeType.WorkingSet);
}
}
@@ -213,7 +213,7 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio
if (requestId) {
this._snapshots.set(requestId, snapshot);
for (const workingSetItem of this._workingSet.keys()) {
this._workingSet.set(workingSetItem, WorkingSetEntryState.Sent);
this._workingSet.set(workingSetItem, { state: WorkingSetEntryState.Sent });
}
const linearHistory = this._linearHistory.get();
const linearHistoryIndex = this._linearHistoryIndex.get();
@@ -229,7 +229,7 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio
}
private _createSnapshot(requestId: string | undefined): IChatEditingSessionSnapshot {
const workingSet = new ResourceMap<WorkingSetEntryState>();
const workingSet = new ResourceMap<WorkingSetDisplayMetadata>();
for (const [file, state] of this._workingSet) {
workingSet.set(file, state);
}
@@ -329,7 +329,7 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio
continue;
}
didRemoveUris = this._workingSet.delete(uri) || didRemoveUris;
if (state === WorkingSetEntryState.Transient) {
if (state.state === WorkingSetEntryState.Transient || state.state === WorkingSetEntryState.Suggested) {
this._removedTransientEntries.add(uri);
}
}
@@ -338,7 +338,7 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio
return; // noop
}
this._onDidChange.fire();
this._onDidChange.fire(ChatEditingSessionChangeType.WorkingSet);
}
private _assertNotDisposed(): void {
@@ -361,7 +361,7 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio
}
}
this._onDidChange.fire();
this._onDidChange.fire(ChatEditingSessionChangeType.Other);
}
async reject(...uris: URI[]): Promise<void> {
@@ -378,7 +378,7 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio
}
}
this._onDidChange.fire();
this._onDidChange.fire(ChatEditingSessionChangeType.Other);
}
async show(): Promise<void> {
@@ -468,11 +468,14 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio
this._sequencer.queue(() => this._resolve());
}
addFileToWorkingSet(resource: URI) {
addFileToWorkingSet(resource: URI, description?: string, proposedState?: WorkingSetEntryState.Suggested): void {
const state = this._workingSet.get(resource);
if (state === undefined || state === WorkingSetEntryState.Transient) {
this._workingSet.set(resource, WorkingSetEntryState.Attached);
this._onDidChange.fire();
if (!state && proposedState === WorkingSetEntryState.Suggested) {
this._workingSet.set(resource, { description, state: WorkingSetEntryState.Suggested });
this._onDidChange.fire(ChatEditingSessionChangeType.WorkingSet);
} else if (state === undefined || state.state === WorkingSetEntryState.Transient) {
this._workingSet.set(resource, { description, state: WorkingSetEntryState.Attached });
this._onDidChange.fire(ChatEditingSessionChangeType.WorkingSet);
}
}
@@ -551,7 +554,7 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio
}
this._state.set(ChatEditingSessionState.Idle, tx);
});
this._onDidChange.fire();
this._onDidChange.fire(ChatEditingSessionChangeType.Other);
}
private async _getOrCreateModifiedFileEntry(resource: URI, responseModel: IModifiedEntryTelemetryInfo): Promise<ChatEditingModifiedFileEntry> {
@@ -576,11 +579,11 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio
this._entriesObs.set(newEntries, undefined);
this._workingSet.delete(entry.modifiedURI);
entry.dispose();
this._onDidChange.fire();
this._onDidChange.fire(ChatEditingSessionChangeType.WorkingSet);
}));
const entriesArr = [...this._entriesObs.get(), entry];
this._entriesObs.set(entriesArr, undefined);
this._onDidChange.fire();
this._onDidChange.fire(ChatEditingSessionChangeType.WorkingSet);
return entry;
}
@@ -614,6 +617,6 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio
export interface IChatEditingSessionSnapshot {
requestId: string | undefined;
workingSet: ResourceMap<WorkingSetEntryState>;
workingSet: ResourceMap<WorkingSetDisplayMetadata>;
entries: ResourceMap<ISnapshotEntry>;
}
@@ -267,12 +267,11 @@ export class ChatEditingSaveAllAction extends Action2 {
id: MenuId.ChatEditingWidgetToolbar,
group: 'navigation',
order: 2,
// Show the option to save without accepting if the user has autosave
// and also hasn't configured the setting to always save with generated changes
// 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()),
ContextKeyExpr.notEquals('config.files.autoSave', 'off'), ContextKeyExpr.equals(`config.${ChatEditorSaving._config}`, false),
ContextKeyExpr.equals(`config.${ChatEditorSaving._config}`, false),
ChatContextKeys.location.isEqualTo(ChatAgentLocation.EditingSession)
)
}
@@ -26,7 +26,6 @@ import { Disposable, DisposableStore, IDisposable, MutableDisposable } from '../
import { ResourceSet } from '../../../../base/common/map.js';
import { basename, dirname } from '../../../../base/common/path.js';
import { isMacintosh } from '../../../../base/common/platform.js';
import type { Mutable } from '../../../../base/common/types.js';
import { URI } from '../../../../base/common/uri.js';
import { IEditorConstructionOptions } from '../../../../editor/browser/config/editorConfiguration.js';
import { EditorExtensionsRegistry } from '../../../../editor/browser/editorExtensions.js';
@@ -34,7 +33,7 @@ import { CodeEditorWidget } from '../../../../editor/browser/widget/codeEditor/c
import { EditorOptions } from '../../../../editor/common/config/editorOptions.js';
import { IDimension } from '../../../../editor/common/core/dimension.js';
import { IPosition } from '../../../../editor/common/core/position.js';
import { Range } from '../../../../editor/common/core/range.js';
import { IRange, Range } from '../../../../editor/common/core/range.js';
import { ILanguageService } from '../../../../editor/common/languages/language.js';
import { ITextModel } from '../../../../editor/common/model.js';
import { IModelService } from '../../../../editor/common/services/model.js';
@@ -53,7 +52,7 @@ import { ICommandService } from '../../../../platform/commands/common/commands.j
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js';
import type { ITextEditorOptions } from '../../../../platform/editor/common/editor.js';
import { ITextEditorOptions } from '../../../../platform/editor/common/editor.js';
import { FileKind, IFileService } from '../../../../platform/files/common/files.js';
import { registerAndCreateHistoryNavigationContext } from '../../../../platform/history/browser/contextScopedHistoryWidget.js';
import { IHoverService } from '../../../../platform/hover/browser/hover.js';
@@ -64,14 +63,15 @@ 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 { IThemeService } from '../../../../platform/theme/common/themeService.js';
import { FolderThemeIcon, IThemeService } from '../../../../platform/theme/common/themeService.js';
import { fillEditorsDragData } from '../../../browser/dnd.js';
import { ResourceLabels } from '../../../browser/labels.js';
import { IFileLabelOptions, ResourceLabels } from '../../../browser/labels.js';
import { ResourceContextKey } from '../../../common/contextkeys.js';
import { ACTIVE_GROUP, IEditorService, SIDE_GROUP } from '../../../services/editor/common/editorService.js';
import { AccessibilityVerbositySettingId } from '../../accessibility/browser/accessibilityConfiguration.js';
import { AccessibilityCommandId } from '../../accessibility/common/accessibilityCommands.js';
import { getSimpleCodeEditorWidgetOptions, getSimpleEditorOptions, setupSimpleEditorSelectionStyling } from '../../codeEditor/browser/simpleEditorOptions.js';
import { revealInsideBarCommand } from '../../files/browser/fileActions.contribution.js';
import { ChatAgentLocation, IChatAgentService } from '../common/chatAgents.js';
import { ChatContextKeys } from '../common/chatContextKeys.js';
import { ChatEditingSessionState, IChatEditingService, IChatEditingSession, WorkingSetEntryState } from '../common/chatEditingService.js';
@@ -267,6 +267,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
@IChatEditingService private readonly chatEditingService: IChatEditingService,
@IMenuService private readonly menuService: IMenuService,
@ILanguageService private readonly languageService: ILanguageService,
@IThemeService private readonly themeService: IThemeService,
) {
super();
@@ -722,6 +723,10 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
};
this._register(this._inputEditor.onDidChangeCursorPosition(e => onDidChangeCursorPosition()));
onDidChangeCursorPosition();
this._register(this.themeService.onDidFileIconThemeChange(() => {
this.renderAttachedContext();
}));
}
private async renderAttachedContext() {
@@ -753,27 +758,32 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
let ariaLabel: string | undefined;
const file = URI.isUri(attachment.value) ? attachment.value : attachment.value && typeof attachment.value === 'object' && 'uri' in attachment.value && URI.isUri(attachment.value.uri) ? attachment.value.uri : undefined;
const resource = URI.isUri(attachment.value) ? attachment.value : attachment.value && typeof attachment.value === 'object' && 'uri' in attachment.value && URI.isUri(attachment.value.uri) ? attachment.value.uri : undefined;
const range = attachment.value && typeof attachment.value === 'object' && 'range' in attachment.value && Range.isIRange(attachment.value.range) ? attachment.value.range : undefined;
if (file && attachment.isFile) {
const fileBasename = basename(file.path);
const fileDirname = dirname(file.path);
if (resource && (attachment.isFile || attachment.isDirectory)) {
const fileBasename = basename(resource.path);
const fileDirname = dirname(resource.path);
const friendlyName = `${fileBasename} ${fileDirname}`;
ariaLabel = range ? localize('chat.fileAttachmentWithRange', "Attached file, {0}, line {1} to line {2}", friendlyName, range.startLineNumber, range.endLineNumber) : localize('chat.fileAttachment', "Attached file, {0}", friendlyName);
label.setFile(file, {
const fileOptions: IFileLabelOptions = { hidePath: true };
label.setFile(resource, attachment.isFile ? {
...fileOptions,
fileKind: FileKind.FILE,
hidePath: true,
range,
} : {
...fileOptions,
fileKind: FileKind.FOLDER,
icon: !this.themeService.getFileIconTheme().hasFolderIcons ? FolderThemeIcon : undefined
});
const scopedContextKeyService = store.add(this.contextKeyService.createScoped(widget));
const resourceContextKey = store.add(new ResourceContextKey(scopedContextKeyService, this.fileService, this.languageService, this.modelService));
resourceContextKey.set(file);
resourceContextKey.set(resource);
this.attachButtonAndDisposables(widget, index, attachment, hoverDelegate, {
contextMenuArg: file,
contextMenuArg: resource,
contextKeyService: scopedContextKeyService,
contextMenuId: MenuId.ChatInputResourceAttachmentContext,
});
@@ -781,7 +791,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
// Drag and drop
widget.draggable = true;
this._register(dom.addDisposableListener(widget, 'dragstart', e => {
this.instantiationService.invokeFunction(accessor => fillEditorsDragData(accessor, [file], e));
this.instantiationService.invokeFunction(accessor => fillEditorsDragData(accessor, [resource], e));
e.dataTransfer?.setDragImage(widget, 0, 0);
}));
@@ -834,36 +844,26 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
return;
}
if (file) {
if (resource) {
widget.style.cursor = 'pointer';
store.add(dom.addDisposableListener(widget, dom.EventType.CLICK, (e: MouseEvent) => {
dom.EventHelper.stop(e, true);
const options: Mutable<OpenInternalOptions> = {
fromUserGesture: true
};
if (range) {
const textEditorOptions: ITextEditorOptions = {
selection: range
};
options.editorOptions = textEditorOptions;
if (attachment.isDirectory) {
this.openResource(resource, true);
} else {
this.openResource(resource, false, range);
}
this.openerService.open(file, options);
}));
store.add(dom.addDisposableListener(widget, dom.EventType.KEY_DOWN, (e: KeyboardEvent) => {
const event = new StandardKeyboardEvent(e);
if (event.equals(KeyCode.Enter) || event.equals(KeyCode.Space)) {
dom.EventHelper.stop(e, true);
const options: Mutable<OpenInternalOptions> = {
fromUserGesture: true
};
if (range) {
const textEditorOptions: ITextEditorOptions = {
selection: range
};
options.editorOptions = textEditorOptions;
if (attachment.isDirectory) {
this.openResource(resource, true);
} else {
this.openResource(resource, false, range);
}
this.openerService.open(file, options);
}
}));
}
@@ -877,6 +877,24 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
}
}
private openResource(resource: URI, isDirectory: true): void;
private openResource(resource: URI, isDirectory: false, range: IRange | undefined): void;
private openResource(resource: URI, isDirectory?: boolean, range?: IRange): void {
if (isDirectory) {
// Reveal Directory in explorer
this.commandService.executeCommand(revealInsideBarCommand.id, resource);
return;
}
// Open file in editor
const openTextEditorOptions: ITextEditorOptions | undefined = range ? { selection: range } : undefined;
const options: OpenInternalOptions = {
fromUserGesture: true,
editorOptions: openTextEditorOptions,
};
this.openerService.open(resource, options);
}
private attachButtonAndDisposables(widget: HTMLElement, index: number, attachment: IChatRequestVariableEntry, hoverDelegate: IHoverDelegate, contextMenuOpts?: { contextMenuId: MenuId; contextKeyService: IContextKeyService; contextMenuArg: unknown }) {
const store = this.attachedContextDisposables.value;
if (!store) {
@@ -996,7 +1014,8 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
if (!seenEntries.has(file)) {
entries.unshift({
reference: file,
state: state,
state: state.state,
description: state.description,
kind: 'reference',
});
seenEntries.add(file);
@@ -3,6 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { raceTimeout } from '../../../../../base/common/async.js';
import { CancellationToken } from '../../../../../base/common/cancellation.js';
import { isPatternInWord } from '../../../../../base/common/filters.js';
import { Disposable } from '../../../../../base/common/lifecycle.js';
@@ -486,18 +487,23 @@ class BuiltinDynamicCompletions extends Disposable {
private async addFileEntries(widget: IChatWidget, result: CompletionList, info: { insert: Range; replace: Range; varWord: IWordAtPosition | null }, token: CancellationToken) {
const makeFileCompletionItem = (resource: URI): CompletionItem => {
const makeFileCompletionItem = (resource: URI, description?: string): CompletionItem => {
const basename = this.labelService.getUriBasenameLabel(resource);
const text = `${chatVariableLeader}file:${basename}`;
const uriLabel = this.labelService.getUriLabel(resource, { relative: true });
const labelDescription = description
? localize('fileEntryDescription', '{0} ({1})', uriLabel, description)
: uriLabel;
const sortText = description ? 'z' : '{'; // after `z`
return {
label: { label: basename, description: this.labelService.getUriLabel(resource, { relative: true }) },
label: { label: basename, description: labelDescription },
filterText: `${chatVariableLeader}${basename}`,
insertText: info.varWord?.endColumn === info.replace.endColumn ? `${text} ` : text,
range: info,
kind: CompletionItemKind.File,
sortText: '{', // after `z`
sortText,
command: {
id: BuiltinDynamicCompletions.addReferenceCommand, title: '', arguments: [new ReferenceArgument(widget, {
id: 'vscode.file',
@@ -518,6 +524,20 @@ class BuiltinDynamicCompletions extends Disposable {
const seen = new ResourceSet();
const len = result.suggestions.length;
// RELATED FILES
if (widget.location === ChatAgentLocation.EditingSession && widget.viewModel && this._chatEditingService.currentEditingSessionObs.get()?.chatSessionId === widget.viewModel?.sessionId) {
const relatedFiles = (await raceTimeout(this._chatEditingService.getRelatedFiles(widget.viewModel.sessionId, widget.getInput(), token), 1000)) ?? [];
for (const relatedFileGroup of relatedFiles) {
for (const relatedFile of relatedFileGroup.files) {
if (seen.has(relatedFile.uri)) {
continue;
}
seen.add(relatedFile.uri);
result.suggestions.push(makeFileCompletionItem(relatedFile.uri, relatedFile.description));
}
}
}
// HISTORY
// always take the last N items
for (const item of this.historyService.getHistory()) {
@@ -576,16 +596,6 @@ class BuiltinDynamicCompletions extends Disposable {
}
}
// RELATED FILES
if (widget.location === ChatAgentLocation.EditingSession && widget.viewModel && this._chatEditingService.currentEditingSessionObs.get()?.chatSessionId === widget.viewModel?.sessionId) {
for (const relatedFile of (await this._chatEditingService.getRelatedFiles(widget.viewModel.sessionId, widget.getInput(), token) ?? [])) {
if (seen.has(relatedFile.uri)) {
continue;
}
result.suggestions.push(makeFileCompletionItem(relatedFile.uri));
}
}
// mark results as incomplete because further typing might yield
// in more search results
result.incomplete = true;
@@ -0,0 +1,125 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { CancellationToken } from '../../../../../base/common/cancellation.js';
import { Event } from '../../../../../base/common/event.js';
import { Disposable, DisposableStore } from '../../../../../base/common/lifecycle.js';
import { ResourceSet } from '../../../../../base/common/map.js';
import { URI } from '../../../../../base/common/uri.js';
import { localize } from '../../../../../nls.js';
import { IWorkbenchContribution } from '../../../../common/contributions.js';
import { ChatEditingSessionChangeType, IChatEditingService, WorkingSetEntryState } from '../../common/chatEditingService.js';
import { IChatWidgetService } from '../chat.js';
export class ChatRelatedFilesContribution extends Disposable implements IWorkbenchContribution {
static readonly ID = 'chat.relatedFilesWorkingSet';
private readonly chatEditingSessionDisposables = new DisposableStore();
private _currentRelatedFilesRetrievalOperation: Promise<void> | undefined;
constructor(
@IChatEditingService private readonly chatEditingService: IChatEditingService,
@IChatWidgetService private readonly chatWidgetService: IChatWidgetService
) {
super();
this._handleNewEditingSession();
this._register(this.chatEditingService.onDidCreateEditingSession(() => {
this.chatEditingSessionDisposables.clear();
this._handleNewEditingSession();
}));
}
private _updateRelatedFileSuggestions() {
if (this._currentRelatedFilesRetrievalOperation) {
return;
}
const currentEditingSession = this.chatEditingService.currentEditingSessionObs.get();
if (currentEditingSession) {
const workingSetEntries = currentEditingSession.entries.get();
const sent = workingSetEntries.find(entry => entry.state.get() === WorkingSetEntryState.Sent || entry.state.get() === WorkingSetEntryState.Modified || entry.state.get() === WorkingSetEntryState.Accepted || entry.state.get() === WorkingSetEntryState.Rejected);
if (sent) {
// Do this only for the initial working set state
return;
}
const widget = this.chatWidgetService.getWidgetBySessionId(currentEditingSession.chatSessionId);
if (!widget) {
return;
}
this._currentRelatedFilesRetrievalOperation = this.chatEditingService.getRelatedFiles(currentEditingSession.chatSessionId, widget.getInput(), CancellationToken.None)
.then((files) => {
if (!files?.length) {
return;
}
const currentEditingSession = this.chatEditingService.currentEditingSessionObs.get();
if (!currentEditingSession || currentEditingSession.chatSessionId !== widget.viewModel?.sessionId) {
return; // Might have disposed while we were calculating
}
// Pick up to 2 related files, or however many we can still fit in the working set
const maximumRelatedFiles = Math.min(2, this.chatEditingService.editingSessionFileLimit - widget.input.chatEditWorkingSetFiles.length);
const newSuggestions = new ResourceSet();
for (const group of files) {
for (const file of group.files) {
if (newSuggestions.size >= maximumRelatedFiles) {
break;
}
newSuggestions.add(file.uri);
}
}
// Remove the existing related file suggestions from the working set
const existingSuggestedEntriesToRemove: URI[] = [];
for (const entry of currentEditingSession.workingSet) {
if (entry[1].state === WorkingSetEntryState.Suggested && !newSuggestions.has(entry[0])) {
existingSuggestedEntriesToRemove.push(entry[0]);
}
}
currentEditingSession?.remove(...existingSuggestedEntriesToRemove);
// Add the new related file suggestions to the working set
for (const file of newSuggestions) {
currentEditingSession.addFileToWorkingSet(file, localize('relatedFile', "Suggested File"), WorkingSetEntryState.Suggested);
}
})
.finally(() => {
this._currentRelatedFilesRetrievalOperation = undefined;
});
}
}
private _handleNewEditingSession() {
const currentEditingSession = this.chatEditingService.currentEditingSessionObs.get();
if (!currentEditingSession) {
return;
}
const widget = this.chatWidgetService.getWidgetBySessionId(currentEditingSession.chatSessionId);
if (!widget || widget.viewModel?.sessionId !== currentEditingSession.chatSessionId) {
return;
}
this.chatEditingSessionDisposables.add(currentEditingSession.onDidDispose(() => {
this.chatEditingSessionDisposables.clear();
}));
this._updateRelatedFileSuggestions();
const onDebouncedType = Event.debounce(widget.inputEditor.onDidChangeModelContent, () => null, 3000);
this.chatEditingSessionDisposables.add(onDebouncedType(() => {
this._updateRelatedFileSuggestions();
}));
this.chatEditingSessionDisposables.add(currentEditingSession.onDidChange((e) => {
if (e === ChatEditingSessionChangeType.WorkingSet) {
this._updateRelatedFileSuggestions();
}
}));
}
override dispose() {
this.chatEditingSessionDisposables.dispose();
super.dispose();
}
}
@@ -882,6 +882,7 @@ have to be updated for changes to the rules above, or to support more deeply nes
padding-right: 4px;
padding-left: 2px;
height: calc(100% + 4px);
outline-offset: -4px;
}
.interactive-session .chat-attached-context .chat-attached-context-attachment .monaco-button:hover {
@@ -973,6 +974,10 @@ have to be updated for changes to the rules above, or to support more deeply nes
padding: 0 2px 0 0;
}
.interactive-session .chat-attached-context .chat-attached-context-attachment .monaco-icon-label.predefined-file-icon::before {
padding: 0 0 0 2px;
}
.interactive-session .interactive-item-container.interactive-request .chat-attached-context .chat-attached-context-attachment {
padding-right: 6px;
}
@@ -42,8 +42,9 @@ export interface IChatEditingService {
getSnapshotUri(requestId: string, uri: URI): URI | undefined;
restoreSnapshot(requestId: string | undefined): Promise<void>;
hasRelatedFilesProviders(): boolean;
registerRelatedFilesProvider(handle: number, provider: IChatRelatedFilesProvider): IDisposable;
getRelatedFiles(chatSessionId: string, prompt: string, token: CancellationToken): Promise<readonly IChatRelatedFile[] | undefined>;
getRelatedFiles(chatSessionId: string, prompt: string, token: CancellationToken): Promise<{ group: string; files: IChatRelatedFile[] }[] | undefined>;
}
export interface IChatRequestDraft {
@@ -51,25 +52,32 @@ export interface IChatRequestDraft {
readonly files: readonly URI[];
}
export interface IChatRelatedFileProviderMetadata {
readonly description: string;
}
export interface IChatRelatedFile {
readonly uri: URI;
readonly description: string;
}
export interface IChatRelatedFilesProvider {
readonly description: string;
provideRelatedFiles(chatRequest: IChatRequestDraft, token: CancellationToken): Promise<IChatRelatedFile[] | undefined>;
}
export interface WorkingSetDisplayMetadata { state: WorkingSetEntryState; description?: string }
export interface IChatEditingSession {
readonly chatSessionId: string;
readonly onDidChange: Event<void>;
readonly onDidChange: Event<ChatEditingSessionChangeType>;
readonly onDidDispose: Event<void>;
readonly state: IObservable<ChatEditingSessionState>;
readonly entries: IObservable<readonly IModifiedFileEntry[]>;
readonly hiddenRequestIds: IObservable<readonly string[]>;
readonly workingSet: ResourceMap<WorkingSetEntryState>;
readonly workingSet: ResourceMap<WorkingSetDisplayMetadata>;
readonly isVisible: boolean;
addFileToWorkingSet(uri: URI): void;
addFileToWorkingSet(uri: URI, description?: string, kind?: WorkingSetEntryState.Transient | WorkingSetEntryState.Suggested): void;
show(): Promise<void>;
remove(...uris: URI[]): void;
accept(...uris: URI[]): Promise<void>;
@@ -89,7 +97,13 @@ export const enum WorkingSetEntryState {
Rejected,
Transient,
Attached,
Sent,
Sent, // TODO@joyceerhl remove this
Suggested,
}
export const enum ChatEditingSessionChangeType {
WorkingSet,
Other,
}
export interface IModifiedFileEntry {
@@ -42,6 +42,7 @@ export interface IBaseChatRequestVariableEntry {
*/
isDynamic?: boolean;
isFile?: boolean;
isDirectory?: boolean;
isTool?: boolean;
isImage?: boolean;
}
@@ -10,7 +10,6 @@ import { URI } from '../../../../base/common/uri.js';
import { Range } from '../../../../editor/common/core/range.js';
import { ILanguageService } from '../../../../editor/common/languages/language.js';
import { EndOfLinePreference, ITextModel } from '../../../../editor/common/model.js';
import { IModelService } from '../../../../editor/common/services/model.js';
import { IResolvedTextEditorModel, ITextModelService } from '../../../../editor/common/services/resolverService.js';
import { extractCodeblockUrisFromText, extractVulnerabilitiesFromText, IMarkdownVulnerability } from './annotations.js';
import { IChatRequestViewModel, IChatResponseViewModel, isResponseVM } from './chatViewModel.js';
@@ -28,18 +27,10 @@ interface CodeBlockEntry {
readonly codemapperUri?: URI;
}
type CodeBlockTextModel = {
readonly type: 'incomplete';
readonly value: ITextModel;
} | {
readonly type: 'complete';
readonly value: Promise<IReference<IResolvedTextEditorModel>>;
};
export class CodeBlockModelCollection extends Disposable {
private readonly _models = new Map<string, {
model: CodeBlockTextModel;
model: Promise<IReference<IResolvedTextEditorModel>>;
vulns: readonly IMarkdownVulnerability[];
codemapperUri?: URI;
}>();
@@ -53,7 +44,6 @@ export class CodeBlockModelCollection extends Disposable {
constructor(
@ILanguageService private readonly languageService: ILanguageService,
@IModelService private readonly modelService: IModelService,
@ITextModelService private readonly textModelService: ITextModelService,
) {
super();
@@ -70,7 +60,7 @@ export class CodeBlockModelCollection extends Disposable {
return;
}
return {
model: entry.model.type === 'incomplete' ? Promise.resolve(entry.model.value) : entry.model.value.then(ref => ref.object.textEditorModel),
model: entry.model.then(ref => ref.object.textEditorModel),
vulns: entry.vulns,
codemapperUri: entry.codemapperUri
};
@@ -82,10 +72,10 @@ export class CodeBlockModelCollection extends Disposable {
return existing;
}
const uri = this.getIncompleteModelUri(sessionId, chat, codeBlockIndex);
const model = this.modelService.createModel('', null, uri, true);
const uri = this.getCodeBlockUri(sessionId, chat, codeBlockIndex);
const model = this.textModelService.createModelReference(uri);
this._models.set(this.getKey(sessionId, chat, codeBlockIndex), {
model: { type: 'incomplete', value: model },
model: model,
vulns: [],
codemapperUri: undefined,
});
@@ -98,7 +88,7 @@ export class CodeBlockModelCollection extends Disposable {
this.delete(first);
}
return { model: Promise.resolve(model), vulns: [], codemapperUri: undefined };
return { model: model.then(x => x.object.textEditorModel), vulns: [], codemapperUri: undefined };
}
private delete(key: string) {
@@ -107,21 +97,13 @@ export class CodeBlockModelCollection extends Disposable {
return;
}
this.disposeModel(entry.model);
entry.model.then(ref => ref.object.dispose());
this._models.delete(key);
}
private disposeModel(model: CodeBlockTextModel) {
if (model.type === 'complete') {
model.value.then(ref => ref.dispose());
} else {
this.modelService.destroyModel(model.value.uri);
}
}
clear(): void {
this._models.forEach(async entry => this.disposeModel(entry.model));
this._models.forEach(async entry => (await entry.model).dispose());
this._models.clear();
}
@@ -146,15 +128,10 @@ export class CodeBlockModelCollection extends Disposable {
markCodeBlockCompleted(sessionId: string, chat: IChatRequestViewModel | IChatResponseViewModel, codeBlockIndex: number): void {
const entry = this._models.get(this.getKey(sessionId, chat, codeBlockIndex));
if (!entry || entry.model.type === 'complete') {
if (!entry) {
return;
}
this.disposeModel(entry.model);
const uri = this.getCompletedModelUri(sessionId, chat, codeBlockIndex);
const newModel = this.textModelService.createModelReference(uri);
entry.model = { type: 'complete', value: newModel };
// TODO: fill this in once we've implemented https://github.com/microsoft/vscode/issues/232538
}
async update(sessionId: string, chat: IChatRequestViewModel | IChatResponseViewModel, codeBlockIndex: number, content: CodeBlockContent): Promise<CodeBlockEntry> {
@@ -222,15 +199,7 @@ export class CodeBlockModelCollection extends Disposable {
return `${sessionId}/${chat.id}/${index}`;
}
private getIncompleteModelUri(sessionId: string, chat: IChatRequestViewModel | IChatResponseViewModel, index: number): URI {
return URI.from({
scheme: Schemas.inMemory,
authority: 'chat-code-block',
path: `/${sessionId}/${chat.id}/${index}`
});
}
private getCompletedModelUri(sessionId: string, chat: IChatRequestViewModel | IChatResponseViewModel, index: number): URI {
private getCodeBlockUri(sessionId: string, chat: IChatRequestViewModel | IChatResponseViewModel, index: number): URI {
const metadata = this.getUriMetaData(chat);
return URI.from({
scheme: Schemas.vscodeChatCodeBlock,
@@ -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, UseUnpkgResourceApi, InstallOperation } 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';
@@ -77,6 +77,7 @@ import { CONTEXT_KEYBINDINGS_EDITOR } from '../../preferences/common/preferences
import { ProgressLocation } from '../../../../platform/progress/common/progress.js';
import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js';
import { IConfigurationMigrationRegistry, Extensions as ConfigurationMigrationExtensions } from '../../../common/configuration.js';
import { IProductService } from '../../../../platform/product/common/productService.js';
// Singletons
registerSingleton(IExtensionsWorkbenchService, ExtensionsWorkbenchService, InstantiationType.Eager /* Auto updates extensions */);
@@ -491,7 +492,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi
constructor(
@IExtensionManagementServerService private readonly extensionManagementServerService: IExtensionManagementServerService,
@IExtensionGalleryService extensionGalleryService: IExtensionGalleryService,
@IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService,
@IContextKeyService contextKeyService: IContextKeyService,
@IViewsService private readonly viewsService: IViewsService,
@IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService,
@@ -499,6 +500,10 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IDialogService private readonly dialogService: IDialogService,
@ICommandService private readonly commandService: ICommandService,
@IFileDialogService private readonly fileDialogService: IFileDialogService,
@IProductService private readonly productService: IProductService,
@IUriIdentityService private readonly uriIdentityService: IUriIdentityService,
@INotificationService private readonly notificationService: INotificationService,
) {
super();
const hasGalleryContext = CONTEXT_HAS_GALLERY.bindTo(contextKeyService);
@@ -1582,6 +1587,53 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi
run: async (accessor: ServicesAccessor, id: string) => accessor.get(IPreferencesService).openSettings({ jsonEditor: false, query: `@ext:${id}` })
});
const downloadVSIX = async (extensionId: string, preRelease: boolean) => {
const result = await this.fileDialogService.showOpenDialog({
title: localize('download title', "Select folder to download the VSIX"),
canSelectFiles: false,
canSelectFolders: true,
canSelectMany: false,
openLabel: localize('download', "Download"),
});
if (!result?.[0]) {
return;
}
const [galleryExtension] = await this.extensionGalleryService.getExtensions([{ id: extensionId, preRelease: true }], { compatible: true }, CancellationToken.None);
if (!galleryExtension) {
throw new Error(localize('not found', "Extension '{0}' not found.", extensionId));
}
await this.extensionGalleryService.download(galleryExtension, this.uriIdentityService.extUri.joinPath(result[0], `${galleryExtension.identifier.id}-${galleryExtension.version}.vsix`), InstallOperation.None);
this.notificationService.info(localize('download.completed', "Successfully downloaded the VSIX"));
};
this.registerExtensionAction({
id: 'workbench.extensions.action.download',
title: localize('download VSIX', "Download VSIX"),
menu: {
id: MenuId.ExtensionContext,
when: ContextKeyExpr.and(ContextKeyExpr.equals('extensionStatus', 'uninstalled'), ContextKeyExpr.has('isGalleryExtension')),
order: this.productService.quality === 'stable' ? 0 : 1
},
run: async (accessor: ServicesAccessor, extensionId: string) => {
downloadVSIX(extensionId, false);
}
});
this.registerExtensionAction({
id: 'workbench.extensions.action.downloadPreRelease',
title: localize('download pre-release', "Download Pre-Release VSIX"),
menu: {
id: MenuId.ExtensionContext,
when: ContextKeyExpr.and(ContextKeyExpr.equals('extensionStatus', 'uninstalled'), ContextKeyExpr.has('isGalleryExtension'), ContextKeyExpr.has('extensionHasPreReleaseVersion')),
order: this.productService.quality === 'stable' ? 1 : 0
},
run: async (accessor: ServicesAccessor, extensionId: string) => {
downloadVSIX(extensionId, true);
}
});
this.registerExtensionAction({
id: 'workbench.extensions.action.manageAccountPreferences',
title: localize2('workbench.extensions.action.changeAccountPreference', "Account Preferences"),
@@ -328,9 +328,7 @@ export class ExplorerFindProvider implements IAsyncFindProvider<ExplorerItem> {
this.startHighlightSession();
}
await this.doHighlightFind(pattern, toggles.matchType, token);
return {};
return await this.doHighlightFind(pattern, toggles.matchType, token);
}
if (this.highlightSessionStartState) {
@@ -475,7 +473,7 @@ export class ExplorerFindProvider implements IAsyncFindProvider<ExplorerItem> {
this.highlightSessionStartState = { rootsWithProviders: new Set(roots) };
}
async doHighlightFind(pattern: string, matchType: TreeFindMatchType, token: CancellationToken): Promise<void> {
async doHighlightFind(pattern: string, matchType: TreeFindMatchType, token: CancellationToken): Promise<IAsyncFindResultMetadata> {
if (!this.highlightSessionStartState) {
throw new Error('ExplorerFindProvider: no highlight session state');
}
@@ -484,13 +482,16 @@ export class ExplorerFindProvider implements IAsyncFindProvider<ExplorerItem> {
const searchResults = await this.getSearchResults(pattern, roots, matchType, token);
if (token.isCancellationRequested) {
return;
return {};
}
this.clearHighlights();
for (const { explorerRoot, files, directories } of searchResults) {
this.addWorkspaceHighlightResults(explorerRoot, files.concat(directories));
}
const hitMaxResults = searchResults.some(({ hitMaxResults }) => hitMaxResults);
return { warningMessage: hitMaxResults ? localize('searchMaxResultsWarning', "The result set only contains a subset of all matches. Be more specific in your search to narrow down the results.") : undefined };
}
private addWorkspaceHighlightResults(root: ExplorerItem, resources: URI[]): void {
@@ -565,14 +566,13 @@ export class ExplorerFindProvider implements IAsyncFindProvider<ExplorerItem> {
shouldGlobMatchFilePattern: true,
cacheKey: `explorerfindprovider:${root.name}:${rootIndex}:${this.sessionId}`,
excludePattern: searchExcludePattern,
maxResults: 512
};
let fileResults: ISearchComplete | undefined;
let folderResults: ISearchComplete | undefined;
try {
[fileResults, folderResults] = await Promise.all([
this.searchService.fileSearch({ ...searchOptions, filePattern: `**/${segmentMatchPattern}` }, token),
this.searchService.fileSearch({ ...searchOptions, filePattern: `**/${segmentMatchPattern}`, maxResults: 512 }, token),
this.searchService.fileSearch({ ...searchOptions, filePattern: `**/${segmentMatchPattern}/**` }, token)
]);
} catch (e) {
@@ -908,8 +908,8 @@ export class FilesRenderer implements ICompressibleTreeRenderer<ExplorerItem, Fu
// If there is a fuzzy score, we need to adjust the offset of the score
// to align with the last stat of the compressed label
let fuzzyScore = node.filterData as FuzzyScore;
if (fuzzyScore.length > 2) {
let fuzzyScore = node.filterData as FuzzyScore | undefined;
if (fuzzyScore && fuzzyScore.length > 2) {
const filterDataOffset = labels.join('/').length - labels[labels.length - 1].length;
fuzzyScore = [fuzzyScore[0], fuzzyScore[1] + filterDataOffset, ...fuzzyScore.slice(2)];
}
@@ -752,13 +752,8 @@ registerAction2(class extends Action2 {
category: interactiveWindowCategory,
menu: {
id: MenuId.CommandPalette,
when: InteractiveWindowOpen,
when: InteractiveWindowOpen
},
keybinding: {
when: ContextKeyExpr.and(IS_COMPOSITE_NOTEBOOK, NOTEBOOK_EDITOR_FOCUSED),
weight: KeybindingWeight.WorkbenchContrib + 5,
primary: KeyMod.CtrlCmd | KeyCode.DownArrow
}
});
}
@@ -111,8 +111,8 @@ export class IssueFormService implements IIssueFormService {
const actions = menu.getActions({ renderShortTitle: true }).flatMap(entry => entry[1]);
for (const action of actions) {
try {
if (action.item && 'source' in action.item && action.item.source?.id === extensionId) {
this.extensionIdentifierSet.add(extensionId);
if (action.item && 'source' in action.item && action.item.source?.id.toLowerCase() === extensionId.toLowerCase()) {
this.extensionIdentifierSet.add(extensionId.toLowerCase());
await action.run();
}
} catch (error) {
@@ -0,0 +1,449 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { isEqual } from '../../../../../base/common/resources.js';
import { Disposable, DisposableStore, dispose, toDisposable } from '../../../../../base/common/lifecycle.js';
import { autorun, derived } from '../../../../../base/common/observable.js';
import { IChatEditingService, ChatEditingSessionState } from '../../../chat/common/chatEditingService.js';
import { NotebookTextModel } from '../../common/model/notebookTextModel.js';
import { INotebookEditor } from '../notebookBrowser.js';
import { ThrottledDelayer } from '../../../../../base/common/async.js';
import { CellDiffInfo } from '../diff/notebookDiffViewModel.js';
import { CellKind } from '../../common/notebookCommon.js';
import { ICodeEditor, IViewZone } from '../../../../../editor/browser/editorBrowser.js';
import { IEditorWorkerService } from '../../../../../editor/common/services/editorWorker.js';
import { ILanguageService } from '../../../../../editor/common/languages/language.js';
import { EditorOption } from '../../../../../editor/common/config/editorOptions.js';
import { themeColorFromId } from '../../../../../base/common/themables.js';
import { RenderOptions, LineSource, renderLines } from '../../../../../editor/browser/widget/diffEditor/components/diffEditorViewZones/renderLines.js';
import { diffAddDecoration, diffWholeLineAddDecoration, diffDeleteDecoration } from '../../../../../editor/browser/widget/diffEditor/registrations.contribution.js';
import { IDocumentDiff } from '../../../../../editor/common/diff/documentDiffProvider.js';
import { ITextModel, TrackedRangeStickiness, MinimapPosition, IModelDeltaDecoration, OverviewRulerLane } from '../../../../../editor/common/model.js';
import { ModelDecorationOptions } from '../../../../../editor/common/model/textModel.js';
import { InlineDecoration, InlineDecorationType } from '../../../../../editor/common/viewModel.js';
import { overviewRulerModifiedForeground, minimapGutterModifiedBackground, overviewRulerAddedForeground, minimapGutterAddedBackground, overviewRulerDeletedForeground, minimapGutterDeletedBackground } from '../../../scm/browser/dirtydiffDecorator.js';
import { Range } from '../../../../../editor/common/core/range.js';
import { NotebookCellTextModel } from '../../common/model/notebookCellTextModel.js';
import { tokenizeToString } from '../../../../../editor/common/languages/textToHtmlTokenizer.js';
import * as DOM from '../../../../../base/browser/dom.js';
import { createTrustedTypesPolicy } from '../../../../../base/browser/trustedTypes.js';
import { splitLines } from '../../../../../base/common/strings.js';
import { DefaultLineHeight } from '../diff/diffElementViewModel.js';
import { INotebookOriginalCellModelFactory } from './notebookOriginalCellModelFactory.js';
export class NotebookCellDiffDecorator extends DisposableStore {
private readonly _decorations = this.editor.createDecorationsCollection();
private _viewZones: string[] = [];
private readonly throttledDecorator = new ThrottledDelayer(100);
constructor(
public readonly editor: ICodeEditor,
private readonly originalCellValue: string,
private readonly cellKind: CellKind,
@IChatEditingService private readonly _chatEditingService: IChatEditingService,
@IEditorWorkerService private readonly _editorWorkerService: IEditorWorkerService,
@INotebookOriginalCellModelFactory private readonly originalCellModelFactory: INotebookOriginalCellModelFactory,
) {
super();
this.add(this.editor.onDidChangeModel(() => this.update()));
this.add(this.editor.onDidChangeConfiguration((e) => {
if (e.hasChanged(EditorOption.fontInfo) || e.hasChanged(EditorOption.lineHeight)) {
this.update();
}
}));
const shouldBeReadOnly = derived(this, r => {
const value = this._chatEditingService.currentEditingSessionObs.read(r);
if (!value || value.state.read(r) !== ChatEditingSessionState.StreamingEdits) {
return false;
}
return value.entries.read(r).some(e => isEqual(e.modifiedURI, this.editor.getModel()?.uri));
});
let actualReadonly: boolean | undefined;
let actualDeco: 'off' | 'editable' | 'on' | undefined;
this.add(autorun(r => {
const value = shouldBeReadOnly.read(r);
if (value) {
actualReadonly ??= this.editor.getOption(EditorOption.readOnly);
actualDeco ??= this.editor.getOption(EditorOption.renderValidationDecorations);
this.editor.updateOptions({
readOnly: true,
renderValidationDecorations: 'off'
});
} else {
if (actualReadonly !== undefined && actualDeco !== undefined) {
this.editor.updateOptions({
readOnly: actualReadonly,
renderValidationDecorations: actualDeco
});
actualReadonly = undefined;
actualDeco = undefined;
}
}
}));
this.update();
}
override dispose(): void {
this._clearRendering();
super.dispose();
}
public update(): void {
this.throttledDecorator.trigger(() => this._updateImpl());
}
private async _updateImpl() {
if (this.isDisposed) {
return;
}
if (!this.editor.hasModel()) {
this._clearRendering();
return;
}
if (this.editor.getOption(EditorOption.inDiffEditor)) {
this._clearRendering();
return;
}
const model = this.editor.getModel();
if (!model) {
this._clearRendering();
return;
}
const version = model.getVersionId();
const originalModel = this.getOrCreateOriginalModel();
const diff = originalModel ? await this.computeDiff() : undefined;
if (this.isDisposed) {
return;
}
if (diff && originalModel && model === this.editor.getModel() && this.editor.getModel()?.getVersionId() === version) {
this._updateWithDiff(originalModel, diff);
} else {
this._clearRendering();
}
}
private _clearRendering() {
this.editor.changeViewZones((viewZoneChangeAccessor) => {
for (const id of this._viewZones) {
viewZoneChangeAccessor.removeZone(id);
}
});
this._viewZones = [];
this._decorations.clear();
}
private _originalModel?: ITextModel;
private getOrCreateOriginalModel() {
if (!this._originalModel) {
const model = this.editor.getModel();
if (!model) {
return;
}
this._originalModel = this.add(this.originalCellModelFactory.getOrCreate(model.uri, this.originalCellValue, model.getLanguageId(), this.cellKind)).object;
}
return this._originalModel;
}
private async computeDiff() {
const model = this.editor.getModel();
if (!model) {
return;
}
const originalModel = this.getOrCreateOriginalModel();
if (!originalModel) {
return;
}
return this._editorWorkerService.computeDiff(
originalModel.uri,
model.uri,
{ computeMoves: true, ignoreTrimWhitespace: false, maxComputationTimeMs: Number.MAX_SAFE_INTEGER },
'advanced'
);
}
private _updateWithDiff(originalModel: ITextModel | undefined, diff: IDocumentDiff): void {
const chatDiffAddDecoration = ModelDecorationOptions.createDynamic({
...diffAddDecoration,
stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges
});
const chatDiffWholeLineAddDecoration = ModelDecorationOptions.createDynamic({
...diffWholeLineAddDecoration,
stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,
});
const createOverviewDecoration = (overviewRulerColor: string, minimapColor: string) => {
return ModelDecorationOptions.createDynamic({
description: 'chat-editing-decoration',
overviewRuler: { color: themeColorFromId(overviewRulerColor), position: OverviewRulerLane.Left },
minimap: { color: themeColorFromId(minimapColor), position: MinimapPosition.Gutter },
});
};
const modifiedDecoration = createOverviewDecoration(overviewRulerModifiedForeground, minimapGutterModifiedBackground);
const addedDecoration = createOverviewDecoration(overviewRulerAddedForeground, minimapGutterAddedBackground);
const deletedDecoration = createOverviewDecoration(overviewRulerDeletedForeground, minimapGutterDeletedBackground);
this.editor.changeViewZones((viewZoneChangeAccessor) => {
for (const id of this._viewZones) {
viewZoneChangeAccessor.removeZone(id);
}
this._viewZones = [];
const modifiedDecorations: IModelDeltaDecoration[] = [];
const mightContainNonBasicASCII = originalModel?.mightContainNonBasicASCII();
const mightContainRTL = originalModel?.mightContainRTL();
const renderOptions = RenderOptions.fromEditor(this.editor);
for (const diffEntry of diff.changes) {
const originalRange = diffEntry.original;
if (originalModel) {
originalModel.tokenization.forceTokenization(Math.max(1, originalRange.endLineNumberExclusive - 1));
}
const source = new LineSource(
(originalRange.length && originalModel) ? originalRange.mapToLineArray(l => originalModel.tokenization.getLineTokens(l)) : [],
[],
mightContainNonBasicASCII,
mightContainRTL,
);
const decorations: InlineDecoration[] = [];
for (const i of diffEntry.innerChanges || []) {
decorations.push(new InlineDecoration(
i.originalRange.delta(-(diffEntry.original.startLineNumber - 1)),
diffDeleteDecoration.className!,
InlineDecorationType.Regular
));
modifiedDecorations.push({
range: i.modifiedRange, options: chatDiffAddDecoration
});
}
if (!diffEntry.modified.isEmpty) {
modifiedDecorations.push({
range: diffEntry.modified.toInclusiveRange()!, options: chatDiffWholeLineAddDecoration
});
}
if (diffEntry.original.isEmpty) {
// insertion
modifiedDecorations.push({
range: diffEntry.modified.toInclusiveRange()!,
options: addedDecoration
});
} else if (diffEntry.modified.isEmpty) {
// deletion
modifiedDecorations.push({
range: new Range(diffEntry.modified.startLineNumber - 1, 1, diffEntry.modified.startLineNumber, 1),
options: deletedDecoration
});
} else {
// modification
modifiedDecorations.push({
range: diffEntry.modified.toInclusiveRange()!,
options: modifiedDecoration
});
}
const domNode = document.createElement('div');
domNode.className = 'chat-editing-original-zone view-lines line-delete monaco-mouse-cursor-text';
const result = renderLines(source, renderOptions, decorations, domNode);
const isCreatedContent = decorations.length === 1 && decorations[0].range.isEmpty() && decorations[0].range.startLineNumber === 1;
if (!isCreatedContent) {
const viewZoneData: IViewZone = {
afterLineNumber: diffEntry.modified.startLineNumber - 1,
heightInLines: result.heightInLines,
domNode,
ordinal: 50000 + 2 // more than https://github.com/microsoft/vscode/blob/bf52a5cfb2c75a7327c9adeaefbddc06d529dcad/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts#L42
};
this._viewZones.push(viewZoneChangeAccessor.addZone(viewZoneData));
}
}
this._decorations.set(modifiedDecorations);
});
}
}
export class NotebookInsertedCellDecorator extends Disposable {
private readonly decorators = this._register(new DisposableStore());
constructor(
private readonly notebookEditor: INotebookEditor,
) {
super();
}
public apply(diffInfo: CellDiffInfo[]) {
const model = this.notebookEditor.textModel;
if (!model) {
return;
}
const cells = diffInfo.filter(diff => diff.type === 'insert').map((diff) => model.cells[diff.modifiedCellIndex]);
const ids = this.notebookEditor.deltaCellDecorations([], cells.map(cell => ({
handle: cell.handle,
options: { className: 'nb-insertHighlight', outputClassName: 'nb-insertHighlight' }
})));
this.clear();
this.decorators.add(toDisposable(() => {
if (!this.notebookEditor.isDisposed) {
this.notebookEditor.deltaCellDecorations(ids, []);
}
}));
}
public clear() {
this.decorators.clear();
}
}
const ttPolicy = createTrustedTypesPolicy('notebookChatEditController', { createHTML: value => value });
export class NotebookDeletedCellDecorator extends Disposable {
private readonly zoneRemover = this._register(new DisposableStore());
private readonly createdViewZones = new Map<number, string>();
constructor(
private readonly _notebookEditor: INotebookEditor,
@ILanguageService private readonly languageService: ILanguageService,
) {
super();
}
public apply(diffInfo: CellDiffInfo[], original: NotebookTextModel): void {
this.clear();
let currentIndex = 0;
const deletedCellsToRender: { cells: NotebookCellTextModel[]; index: number } = { cells: [], index: 0 };
diffInfo.forEach(diff => {
if (diff.type === 'delete') {
const deletedCell = original.cells[diff.originalCellIndex];
if (deletedCell) {
deletedCellsToRender.cells.push(deletedCell);
deletedCellsToRender.index = currentIndex;
}
} else {
if (deletedCellsToRender.cells.length) {
this._createWidget(deletedCellsToRender.index + 1, deletedCellsToRender.cells);
deletedCellsToRender.cells.length = 0;
}
currentIndex = diff.modifiedCellIndex;
}
});
if (deletedCellsToRender.cells.length) {
this._createWidget(deletedCellsToRender.index + 1, deletedCellsToRender.cells);
}
}
public clear() {
this.zoneRemover.clear();
}
private _createWidget(index: number, cells: NotebookCellTextModel[]) {
this._createWidgetImpl(index, cells);
}
private async _createWidgetImpl(index: number, cells: NotebookCellTextModel[]) {
const rootContainer = document.createElement('div');
const widgets = cells.map(cell => new NotebookDeletedCellWidget(this._notebookEditor, cell.getValue(), cell.language, rootContainer, this.languageService));
const heights = await Promise.all(widgets.map(w => w.render()));
const totalHeight = heights.reduce<number>((prev, curr) => prev + curr, 0);
this._notebookEditor.changeViewZones(accessor => {
const notebookViewZone = {
afterModelPosition: index,
heightInPx: totalHeight + 4,
domNode: rootContainer
};
const id = accessor.addZone(notebookViewZone);
accessor.layoutZone(id);
this.createdViewZones.set(index, id);
this.zoneRemover.add(toDisposable(() => {
if (this.createdViewZones.get(index) === id) {
this.createdViewZones.delete(index);
}
if (!this._notebookEditor.isDisposed) {
this._notebookEditor.changeViewZones(accessor => {
accessor.removeZone(id);
dispose(widgets);
});
}
}));
});
}
}
export class NotebookDeletedCellWidget extends Disposable {
private readonly container: HTMLElement;
constructor(
private readonly _notebookEditor: INotebookEditor,
// private readonly _index: number,
private readonly code: string,
private readonly language: string,
container: HTMLElement,
@ILanguageService private readonly languageService: ILanguageService,
) {
super();
this.container = DOM.append(container, document.createElement('div'));
this._register(toDisposable(() => {
container.removeChild(this.container);
}));
}
public async render() {
const code = this.code;
const languageId = this.language;
const codeHtml = await tokenizeToString(this.languageService, code, languageId);
// const colorMap = this.getDefaultColorMap();
const fontInfo = this._notebookEditor.getBaseCellEditorOptions(languageId).value;
const fontFamilyVar = '--notebook-editor-font-family';
const fontSizeVar = '--notebook-editor-font-size';
const fontWeightVar = '--notebook-editor-font-weight';
// If we have any editors, then use left layout of one of those.
const editor = this._notebookEditor.codeEditors.map(c => c[1]).find(c => c);
const layoutInfo = editor?.getOptions().get(EditorOption.layoutInfo);
const style = ``
+ `font-family: var(${fontFamilyVar});`
+ `font-weight: var(${fontWeightVar});`
+ `font-size: var(${fontSizeVar});`
+ fontInfo.lineHeight ? `line-height: ${fontInfo.lineHeight}px;` : ''
+ layoutInfo?.contentLeft ? `margin-left: ${layoutInfo}px;` : ''
+ `white-space: pre;`;
const rootContainer = this.container;
rootContainer.classList.add('code-cell-row');
const container = DOM.append(rootContainer, DOM.$('.cell-inner-container'));
const focusIndicatorLeft = DOM.append(container, DOM.$('.cell-focus-indicator.cell-focus-indicator-side.cell-focus-indicator-left'));
const cellContainer = DOM.append(container, DOM.$('.cell.code'));
DOM.append(focusIndicatorLeft, DOM.$('div.execution-count-label'));
const editorPart = DOM.append(cellContainer, DOM.$('.cell-editor-part'));
let editorContainer = DOM.append(editorPart, DOM.$('.cell-editor-container'));
editorContainer = DOM.append(editorContainer, DOM.$('.code', { style }));
if (fontInfo.fontFamily) {
editorContainer.style.setProperty(fontFamilyVar, fontInfo.fontFamily);
}
if (fontInfo.fontSize) {
editorContainer.style.setProperty(fontSizeVar, `${fontInfo.fontSize}px`);
}
if (fontInfo.fontWeight) {
editorContainer.style.setProperty(fontWeightVar, fontInfo.fontWeight);
}
editorContainer.innerHTML = (ttPolicy?.createHTML(codeHtml) || codeHtml) as string;
const lineCount = splitLines(code).length;
const height = (lineCount * (fontInfo.lineHeight || DefaultLineHeight)) + 12 + 12; // We have 12px top and bottom in generated code HTML;
const totalHeight = height + 16 + 16;
return totalHeight;
}
}
@@ -0,0 +1,164 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Disposable, DisposableStore, MutableDisposable, toDisposable } from '../../../../../base/common/lifecycle.js';
import { INotebookEditor } from '../notebookBrowser.js';
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
import { HiddenItemStrategy, MenuWorkbenchToolBar } from '../../../../../platform/actions/browser/toolbar.js';
import { MenuId } from '../../../../../platform/actions/common/actions.js';
import { ActionViewItem } from '../../../../../base/browser/ui/actionbar/actionViewItems.js';
import { ActionRunner, IAction, IActionRunner } from '../../../../../base/common/actions.js';
import { $ } from '../../../../../base/browser/dom.js';
import { IChatEditingService, IModifiedFileEntry } from '../../../chat/common/chatEditingService.js';
import { ACTIVE_GROUP, IEditorService } from '../../../../services/editor/common/editorService.js';
import { Range } from '../../../../../editor/common/core/range.js';
import { autorunWithStore, observableFromEvent } from '../../../../../base/common/observable.js';
import { isEqual } from '../../../../../base/common/resources.js';
export class NotebookChatActionsOverlayController extends Disposable {
constructor(
private readonly notebookEditor: INotebookEditor,
@IChatEditingService private readonly _chatEditingService: IChatEditingService,
@IInstantiationService instantiationService: IInstantiationService,
) {
super();
const notebookModel = observableFromEvent(this.notebookEditor.onDidChangeModel, e => e);
this._register(autorunWithStore((r, store) => {
const session = this._chatEditingService.currentEditingSessionObs.read(r);
const model = notebookModel.read(r);
if (!model || !session) {
return;
}
const entries = session.entries.read(r);
const idx = entries.findIndex(e => isEqual(e.modifiedURI, model.uri));
if (idx >= 0) {
const entry = entries[idx];
const nextEntry = entries[(idx + 1) % entries.length];
const previousEntry = entries[(idx - 1 + entries.length) % entries.length];
store.add(instantiationService.createInstance(NotebookChatActionsOverlay, notebookEditor, entry, nextEntry, previousEntry));
}
}));
}
}
// Copied from src/vs/workbench/contrib/chat/browser/chatEditorOverlay.ts (until we unify these)
export class NotebookChatActionsOverlay extends Disposable {
constructor(
notebookEditor: INotebookEditor,
entry: IModifiedFileEntry,
nextEntry: IModifiedFileEntry,
previousEntry: IModifiedFileEntry,
@IEditorService private readonly _editorService: IEditorService,
@IInstantiationService instaService: IInstantiationService,
) {
super();
const toolbarNode = $('div');
toolbarNode.classList.add('notebook-chat-editor-overlay-widget');
notebookEditor.getDomNode().appendChild(toolbarNode);
this._register(toDisposable(() => {
notebookEditor.getDomNode().removeChild(toolbarNode);
}));
const _toolbar = instaService.createInstance(MenuWorkbenchToolBar, toolbarNode, MenuId.ChatEditingEditorContent, {
telemetrySource: 'chatEditor.overlayToolbar',
hiddenItemStrategy: HiddenItemStrategy.Ignore,
toolbarOptions: {
primaryGroup: () => true,
useSeparatorsInPrimaryActions: true
},
menuOptions: { renderShortTitle: true },
actionViewItemProvider: (action, options) => {
const that = this;
if (action.id === 'chatEditor.action.accept' || action.id === 'chatEditor.action.reject') {
return new class extends ActionViewItem {
private readonly _reveal = this._store.add(new MutableDisposable());
constructor() {
super(undefined, action, { ...options, icon: false, label: true, keybindingNotRenderedWithLabel: true });
}
override set actionRunner(actionRunner: IActionRunner) {
super.actionRunner = actionRunner;
const store = new DisposableStore();
store.add(actionRunner.onWillRun(_e => {
notebookEditor.focus();
}));
store.add(actionRunner.onDidRun(e => {
if (e.action !== this.action) {
return;
}
if (entry === nextEntry) {
return;
}
const change = nextEntry.diffInfo.get().changes.at(0);
return that._editorService.openEditor({
resource: nextEntry.modifiedURI,
options: {
selection: change && Range.fromPositions({ lineNumber: change.original.startLineNumber, column: 1 }),
revealIfOpened: false,
revealIfVisible: false,
}
}, ACTIVE_GROUP);
}));
this._reveal.value = store;
}
override get actionRunner(): IActionRunner {
return super.actionRunner;
}
};
}
// Override next/previous with our implementation.
if (action.id === 'chatEditor.action.navigateNext' || action.id === 'chatEditor.action.navigatePrevious') {
return new class extends ActionViewItem {
constructor() {
super(undefined, action, { ...options, icon: true, label: false, keybindingNotRenderedWithLabel: true });
}
override set actionRunner(_: IActionRunner) {
const next = action.id === 'chatEditor.action.navigateNext' ? nextEntry : previousEntry;
super.actionRunner = new NextPreviousChangeActionRunner(entry, next, _editorService);
}
override get actionRunner(): IActionRunner {
return super.actionRunner;
}
};
}
return undefined;
}
});
this._register(_toolbar);
}
}
class NextPreviousChangeActionRunner extends ActionRunner {
constructor(private readonly entry: IModifiedFileEntry, private readonly next: IModifiedFileEntry, private readonly _editorService: IEditorService) {
super();
}
protected override async runAction(action: IAction, context?: unknown): Promise<void> {
if (this.entry === this.next) {
return;
}
// For now just go to next/previous file.
const change = this.next.diffInfo.get().changes.at(0);
await this._editorService.openEditor({
resource: this.next.modifiedURI,
options: {
selection: change && Range.fromPositions({ lineNumber: change.original.startLineNumber, column: 1 }),
revealIfOpened: false,
revealIfVisible: false,
}
}, ACTIVE_GROUP);
}
}
@@ -0,0 +1,188 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { isEqual } from '../../../../../base/common/resources.js';
import { Disposable, dispose, IDisposable, toDisposable } from '../../../../../base/common/lifecycle.js';
import { autorun, derived, derivedWithStore, observableFromEvent, observableValue } from '../../../../../base/common/observable.js';
import { IChatEditingService, WorkingSetEntryState } from '../../../chat/common/chatEditingService.js';
import { NotebookTextModel } from '../../common/model/notebookTextModel.js';
import { INotebookEditor, INotebookEditorContribution } from '../notebookBrowser.js';
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
import { ICodeEditor } from '../../../../../editor/browser/editorBrowser.js';
import { NotebookCellTextModel } from '../../common/model/notebookCellTextModel.js';
import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
import { NotebookDeletedCellDecorator, NotebookInsertedCellDecorator, NotebookCellDiffDecorator } from './notebookCellDecorators.js';
import { INotebookModelSynchronizerFactory } from './notebookSynronizer.js';
import { INotebookOriginalModelReferenceFactory } from './notebookOriginalModelRefFactory.js';
import { debouncedObservable2 } from '../../../../../base/common/observableInternal/utils.js';
import { CellDiffInfo } from '../diff/notebookDiffViewModel.js';
import { NotebookChatActionsOverlayController } from './notebookChatActionsOverlay.js';
import { IContextKey, IContextKeyService, RawContextKey } from '../../../../../platform/contextkey/common/contextkey.js';
import { localize } from '../../../../../nls.js';
export const ctxNotebookHasEditorModification = new RawContextKey<boolean>('chat.hasNotebookEditorModifications', undefined, localize('chat.hasNotebookEditorModifications', "The current Notebook editor contains chat modifications"));
export class NotebookChatEditorControllerContrib extends Disposable implements INotebookEditorContribution {
public static readonly ID: string = 'workbench.notebook.chatEditorController';
readonly _serviceBrand: undefined;
constructor(
notebookEditor: INotebookEditor,
@IInstantiationService instantiationService: IInstantiationService,
@IConfigurationService configurationService: IConfigurationService,
) {
super();
if (configurationService.getValue<boolean>('notebook.experimental.chatEdits')) {
this._register(instantiationService.createInstance(NotebookChatEditorController, notebookEditor));
}
}
}
class NotebookChatEditorController extends Disposable {
private readonly deletedCellDecorator: NotebookDeletedCellDecorator;
private readonly insertedCellDecorator: NotebookInsertedCellDecorator;
private readonly _ctxHasEditorModification: IContextKey<boolean>;
constructor(
private readonly notebookEditor: INotebookEditor,
@IChatEditingService private readonly _chatEditingService: IChatEditingService,
@INotebookOriginalModelReferenceFactory private readonly originalModelRefFactory: INotebookOriginalModelReferenceFactory,
@INotebookModelSynchronizerFactory private readonly synchronizerFactory: INotebookModelSynchronizerFactory,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IContextKeyService contextKeyService: IContextKeyService,
) {
super();
this._ctxHasEditorModification = ctxNotebookHasEditorModification.bindTo(contextKeyService);
this._register(instantiationService.createInstance(NotebookChatActionsOverlayController, notebookEditor));
this.deletedCellDecorator = this._register(instantiationService.createInstance(NotebookDeletedCellDecorator, notebookEditor));
this.insertedCellDecorator = this._register(instantiationService.createInstance(NotebookInsertedCellDecorator, notebookEditor));
const notebookModel = observableFromEvent(this.notebookEditor.onDidChangeModel, e => e);
const originalModel = observableValue<NotebookTextModel | undefined>('originalModel', undefined);
const viewModelAttached = observableFromEvent(this.notebookEditor.onDidAttachViewModel, () => !!this.notebookEditor.getViewModel());
const onDidChangeVisibleRanges = debouncedObservable2(observableFromEvent(this.notebookEditor.onDidChangeVisibleRanges, () => this.notebookEditor.visibleRanges), 100);
const decorators = new Map<NotebookCellTextModel, { editor: ICodeEditor } & IDisposable>();
let updatedCellDecoratorsOnceBefore = false;
let updatedDeletedInsertedDecoratorsOnceBefore = false;
const clearDecorators = () => {
dispose(Array.from(decorators.values()));
decorators.clear();
this.deletedCellDecorator.clear();
this.insertedCellDecorator.clear();
};
this._register(toDisposable(() => clearDecorators()));
const entryObs = derived((r) => {
const session = this._chatEditingService.currentEditingSessionObs.read(r);
const model = notebookModel.read(r);
if (!model || !session) {
return;
}
const entry = session.entries.read(r).find(e => isEqual(e.modifiedURI, model.uri));
if (!entry || entry.state.read(r) !== WorkingSetEntryState.Modified) {
clearDecorators();
return;
}
return entry;
}).recomputeInitiallyAndOnChange(this._store);
const snapshotCreated = observableValue<boolean>('snapshotCreated', false);
const diffInfoObs = derivedWithStore(this, (r, store) => {
const entry = entryObs.read(r);
const model = notebookModel.read(r);
if (!entry || !model) {
return observableValue<{
cellDiff: CellDiffInfo[];
modelVersion: number;
} | undefined>('DefaultDiffIno', undefined);
}
const notebookSynchronizer = store.add(this.synchronizerFactory.getOrCreate(model, entry));
// Initialize the observables.
notebookSynchronizer.object.createSnapshot().finally(() => snapshotCreated.set(true, undefined));
this.originalModelRefFactory.getOrCreate(entry, model.viewType).then(ref => originalModel.set(this._register(ref).object, undefined));
return notebookSynchronizer.object.diffInfo;
}).recomputeInitiallyAndOnChange(this._store).flatten();
this._register(autorun(r => {
// If we have a new entry for the file, then clear old decorators.
// User could be cycling through different edit sessions (Undo Last Edit / Redo Last Edit).
entryObs.read(r);
clearDecorators();
}));
this._register(autorun(r => {
// If there's no diff info, then we either accepted or rejected everything.
const diffs = diffInfoObs.read(r);
if (!diffs || !diffs.cellDiff.length) {
clearDecorators();
this._ctxHasEditorModification.reset();
} else {
this._ctxHasEditorModification.set(true);
}
}));
this._register(autorun(r => {
const entry = entryObs.read(r);
const diffInfo = diffInfoObs.read(r);
const modified = notebookModel.read(r);
const original = originalModel.read(r);
onDidChangeVisibleRanges.read(r);
if (!entry || !modified || !original || !diffInfo) {
return;
}
if (diffInfo && updatedCellDecoratorsOnceBefore && (diffInfo.modelVersion !== modified.versionId)) {
return;
}
updatedCellDecoratorsOnceBefore = true;
diffInfo.cellDiff.forEach((diff) => {
if (diff.type === 'modified') {
const modifiedCell = modified.cells[diff.modifiedCellIndex];
const originalCellValue = original.cells[diff.originalCellIndex].getValue();
const editor = this.notebookEditor.codeEditors.find(([vm,]) => vm.handle === modifiedCell.handle)?.[1];
if (editor && decorators.get(modifiedCell)?.editor !== editor) {
decorators.get(modifiedCell)?.dispose();
const decorator = this.instantiationService.createInstance(NotebookCellDiffDecorator, editor, originalCellValue, modifiedCell.cellKind);
decorators.set(modifiedCell, decorator);
this._register(editor.onDidDispose(() => {
decorator.dispose();
if (decorators.get(modifiedCell) === decorator) {
decorators.set(modifiedCell, decorator);
}
}));
}
}
});
}));
this._register(autorun(r => {
const entry = entryObs.read(r);
const diffInfo = diffInfoObs.read(r);
const modified = notebookModel.read(r);
const original = originalModel.read(r);
const vmAttached = viewModelAttached.read(r);
if (!vmAttached || !entry || !modified || !original || !diffInfo) {
return;
}
if (diffInfo && updatedDeletedInsertedDecoratorsOnceBefore && (diffInfo.modelVersion !== modified.versionId)) {
return;
}
updatedDeletedInsertedDecoratorsOnceBefore = true;
this.insertedCellDecorator.apply(diffInfo.cellDiff);
this.deletedCellDecorator.apply(diffInfo.cellDiff, original);
}));
}
}
@@ -0,0 +1,53 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IReference, ReferenceCollection } from '../../../../../base/common/lifecycle.js';
import { createDecorator, IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
import { ITextModel } from '../../../../../editor/common/model.js';
import { CellKind } from '../../common/notebookCommon.js';
import { URI } from '../../../../../base/common/uri.js';
import { ILanguageService } from '../../../../../editor/common/languages/language.js';
import { IModelService } from '../../../../../editor/common/services/model.js';
export const INotebookOriginalCellModelFactory = createDecorator<INotebookOriginalCellModelFactory>('INotebookOriginalCellModelFactory');
export interface INotebookOriginalCellModelFactory {
readonly _serviceBrand: undefined;
getOrCreate(uri: URI, cellValue: string, language: string, cellKind: CellKind): IReference<ITextModel>;
}
export class OriginalNotebookCellModelReferenceCollection extends ReferenceCollection<ITextModel> {
constructor(@IModelService private readonly modelService: IModelService,
@ILanguageService private readonly _languageService: ILanguageService,
) {
super();
}
protected override createReferencedObject(_key: string, uri: URI, cellValue: string, language: string, cellKind: CellKind): ITextModel {
const scheme = `${uri.scheme}-chat-edit`;
const originalCellUri = URI.from({ scheme, fragment: uri.fragment, path: uri.path });
const languageSelection = this._languageService.getLanguageIdByLanguageName(language) ? this._languageService.createById(language) : cellKind === CellKind.Markup ? this._languageService.createById('markdown') : null;
return this.modelService.createModel(cellValue, languageSelection, originalCellUri);
}
protected override destroyReferencedObject(_key: string, model: ITextModel): void {
model.dispose();
}
}
export class OriginalNotebookCellModelFactory implements INotebookOriginalCellModelFactory {
readonly _serviceBrand: undefined;
private readonly _data: OriginalNotebookCellModelReferenceCollection;
constructor(@IInstantiationService instantiationService: IInstantiationService) {
this._data = instantiationService.createInstance(OriginalNotebookCellModelReferenceCollection);
}
getOrCreate(uri: URI, cellValue: string, language: string, cellKind: CellKind): IReference<ITextModel> {
return this._data.acquire(uri.toString(), uri, cellValue, language, cellKind);
}
}
@@ -0,0 +1,90 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { AsyncReferenceCollection, IReference, ReferenceCollection } from '../../../../../base/common/lifecycle.js';
import { IModifiedFileEntry } from '../../../chat/common/chatEditingService.js';
import { INotebookService } from '../../common/notebookService.js';
import { bufferToStream, VSBuffer } from '../../../../../base/common/buffer.js';
import { NotebookTextModel } from '../../common/model/notebookTextModel.js';
import { createDecorator, IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
export const INotebookOriginalModelReferenceFactory = createDecorator<INotebookOriginalModelReferenceFactory>('INotebookOriginalModelReferenceFactory');
export interface INotebookOriginalModelReferenceFactory {
readonly _serviceBrand: undefined;
getOrCreate(fileEntry: IModifiedFileEntry, viewType: string): Promise<IReference<NotebookTextModel>>;
}
export class OriginalNotebookModelReferenceCollection extends ReferenceCollection<Promise<NotebookTextModel>> {
private readonly modelsToDispose = new Set<string>();
constructor(@INotebookService private readonly notebookService: INotebookService) {
super();
}
protected override async createReferencedObject(key: string, fileEntry: IModifiedFileEntry, viewType: string): Promise<NotebookTextModel> {
this.modelsToDispose.delete(key);
const uri = fileEntry.originalURI;
const model = this.notebookService.getNotebookTextModel(uri);
if (model) {
return model;
}
const bytes = VSBuffer.fromString(fileEntry.originalModel.getValue());
const stream = bufferToStream(bytes);
return this.notebookService.createNotebookTextModel(viewType, uri, stream);
}
protected override destroyReferencedObject(key: string, modelPromise: Promise<NotebookTextModel>): void {
this.modelsToDispose.add(key);
(async () => {
try {
const model = await modelPromise;
if (!this.modelsToDispose.has(key)) {
// return if model has been acquired again meanwhile
return;
}
// Finally we can dispose the model
model.dispose();
} catch (error) {
// ignore
} finally {
this.modelsToDispose.delete(key); // Untrack as being disposed
}
})();
}
}
export class NotebookOriginalModelReferenceFactory implements INotebookOriginalModelReferenceFactory {
readonly _serviceBrand: undefined;
private _resourceModelCollection: OriginalNotebookModelReferenceCollection & ReferenceCollection<Promise<NotebookTextModel>> /* TS Fail */ | undefined = undefined;
private get resourceModelCollection() {
if (!this._resourceModelCollection) {
this._resourceModelCollection = this.instantiationService.createInstance(OriginalNotebookModelReferenceCollection);
}
return this._resourceModelCollection;
}
private _asyncModelCollection: AsyncReferenceCollection<NotebookTextModel> | undefined = undefined;
private get asyncModelCollection() {
if (!this._asyncModelCollection) {
this._asyncModelCollection = new AsyncReferenceCollection(this.resourceModelCollection);
}
return this._asyncModelCollection;
}
constructor(@IInstantiationService private readonly instantiationService: IInstantiationService) {
}
getOrCreate(fileEntry: IModifiedFileEntry, viewType: string): Promise<IReference<NotebookTextModel>> {
return this.asyncModelCollection.acquire(fileEntry.originalURI.toString(), fileEntry, viewType);
}
}
@@ -0,0 +1,358 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { isEqual } from '../../../../../base/common/resources.js';
import { Disposable, DisposableStore, IReference, ReferenceCollection } from '../../../../../base/common/lifecycle.js';
import { IModifiedFileEntry } from '../../../chat/common/chatEditingService.js';
import { INotebookService } from '../../common/notebookService.js';
import { bufferToStream, VSBuffer } from '../../../../../base/common/buffer.js';
import { NotebookTextModel } from '../../common/model/notebookTextModel.js';
import { raceCancellation, ThrottledDelayer } from '../../../../../base/common/async.js';
import { CellDiffInfo, computeDiff, prettyChanges } from '../diff/notebookDiffViewModel.js';
import { CancellationToken, CancellationTokenSource } from '../../../../../base/common/cancellation.js';
import { INotebookEditorWorkerService } from '../../common/services/notebookWorkerService.js';
import { ChatEditingModifiedFileEntry } from '../../../chat/browser/chatEditing/chatEditingModifiedFileEntry.js';
import { CellEditType, ICellDto2, ICellReplaceEdit, NotebookData, NotebookSetting } from '../../common/notebookCommon.js';
import { URI } from '../../../../../base/common/uri.js';
import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
import { EditOperation } from '../../../../../editor/common/core/editOperation.js';
import { INotebookLoggingService } from '../../common/notebookLoggingService.js';
import { filter } from '../../../../../base/common/objects.js';
import { INotebookEditorModelResolverService } from '../../common/notebookEditorModelResolverService.js';
import { SaveReason } from '../../../../common/editor.js';
import { IChatService } from '../../../chat/common/chatService.js';
import { createDecorator, IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
import { INotebookOriginalModelReferenceFactory } from './notebookOriginalModelRefFactory.js';
import { IObservable, observableValue } from '../../../../../base/common/observable.js';
export const INotebookModelSynchronizerFactory = createDecorator<INotebookModelSynchronizerFactory>('INotebookModelSynchronizerFactory');
export interface INotebookModelSynchronizerFactory {
readonly _serviceBrand: undefined;
getOrCreate(model: NotebookTextModel, entry: IModifiedFileEntry): IReference<NotebookModelSynchronizer>;
}
class NotebookModelSynchronizerReferenceCollection extends ReferenceCollection<NotebookModelSynchronizer> {
constructor(@IInstantiationService private readonly instantiationService: IInstantiationService) {
super();
}
protected override createReferencedObject(_key: string, model: NotebookTextModel, entry: IModifiedFileEntry): NotebookModelSynchronizer {
return this.instantiationService.createInstance(NotebookModelSynchronizer, model, entry);
}
protected override destroyReferencedObject(_key: string, object: NotebookModelSynchronizer): void {
object.dispose();
}
}
export class NotebookModelSynchronizerFactory implements INotebookModelSynchronizerFactory {
readonly _serviceBrand: undefined;
private readonly _data: NotebookModelSynchronizerReferenceCollection;
constructor(@IInstantiationService instantiationService: IInstantiationService) {
this._data = instantiationService.createInstance(NotebookModelSynchronizerReferenceCollection);
}
getOrCreate(model: NotebookTextModel, entry: IModifiedFileEntry): IReference<NotebookModelSynchronizer> {
return this._data.acquire(entry.modifiedURI.toString(), model, entry);
}
}
export class NotebookModelSynchronizer extends Disposable {
private readonly throttledUpdateNotebookModel = new ThrottledDelayer(200);
private _diffInfo = observableValue<{ cellDiff: CellDiffInfo[]; modelVersion: number } | undefined>('diffInfo', undefined);
public get diffInfo(): IObservable<{ cellDiff: CellDiffInfo[]; modelVersion: number } | undefined> {
return this._diffInfo;
}
private snapshot?: { bytes: VSBuffer; dirty: boolean };
private isEditFromUs: boolean = false;
constructor(
private readonly model: NotebookTextModel,
public readonly entry: IModifiedFileEntry,
@INotebookService private readonly notebookService: INotebookService,
@IChatService chatService: IChatService,
@INotebookLoggingService private readonly logService: INotebookLoggingService,
@IConfigurationService private readonly configurationService: IConfigurationService,
@INotebookEditorWorkerService private readonly notebookEditorWorkerService: INotebookEditorWorkerService,
@INotebookEditorModelResolverService private readonly notebookModelResolverService: INotebookEditorModelResolverService,
@INotebookOriginalModelReferenceFactory private readonly originalModelRefFactory: INotebookOriginalModelReferenceFactory,
) {
super();
this._register(chatService.onDidPerformUserAction(async e => {
if (e.action.kind === 'chatEditingSessionAction' && !e.action.hasRemainingEdits && isEqual(e.action.uri, entry.modifiedURI)) {
if (e.action.outcome === 'accepted') {
await this.accept();
await this.createSnapshot();
this._diffInfo.set(undefined, undefined);
}
else if (e.action.outcome === 'rejected') {
if (await this.revert()) {
this._diffInfo.set(undefined, undefined);
}
}
}
}));
const cancellationTokenStore = this._register(new DisposableStore());
let cancellationToken = cancellationTokenStore.add(new CancellationTokenSource());
const updateNotebookModel = (entry: IModifiedFileEntry, token: CancellationToken) => {
this.throttledUpdateNotebookModel.trigger(() => this.updateNotebookModel(entry, token));
};
const modifiedModel = (entry as ChatEditingModifiedFileEntry).modifiedModel;
this._register(modifiedModel.onDidChangeContent(async () => {
cancellationTokenStore.clear();
if (!modifiedModel.isDisposed() && !entry.originalModel.isDisposed() && modifiedModel.getValue() === entry.originalModel.getValue()) {
if (await this.revert()) {
this._diffInfo.set(undefined, undefined);
}
return;
}
cancellationToken = cancellationTokenStore.add(new CancellationTokenSource());
updateNotebookModel(entry, cancellationToken.token);
}));
this._register(model.onDidChangeContent(() => {
// Track changes from the user.
if (!this.isEditFromUs && this.snapshot) {
this.snapshot.dirty = true;
}
}));
updateNotebookModel(entry, cancellationToken.token);
}
public async createSnapshot() {
const [serializer, ref] = await Promise.all([
this.getNotebookSerializer(),
this.notebookModelResolverService.resolve(this.model.uri)
]);
try {
const data: NotebookData = {
metadata: filter(this.model.metadata, key => !serializer.options.transientDocumentMetadata[key]),
cells: [],
};
let outputSize = 0;
for (const cell of this.model.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<number>(NotebookSetting.outputBackupSizeLimit) * 1024;
if (outputSizeLimit > 0) {
cell.outputs.forEach(output => {
output.outputs.forEach(item => {
outputSize += item.data.byteLength;
});
});
if (outputSize > outputSizeLimit) {
return;
}
}
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);
this.snapshot = { bytes, dirty: ref.object.isDirty() };
} finally {
ref.dispose();
}
}
private async revert(): Promise<boolean> {
if (!this.snapshot) {
return false;
}
await this.updateNotebook(this.snapshot.bytes, !this.snapshot.dirty);
return true;
}
private async updateNotebook(bytes: VSBuffer, save: boolean) {
const ref = await this.notebookModelResolverService.resolve(this.model.uri);
try {
const serializer = await this.getNotebookSerializer();
const data = await serializer.dataToNotebook(bytes);
this.model.reset(data.cells, data.metadata, serializer.options);
if (save) {
// save the file after discarding so that the dirty indicator goes away
// and so that an intermediate saved state gets reverted
// await this.notebookEditor.textModel.save({ reason: SaveReason.EXPLICIT });
await ref.object.save({ reason: SaveReason.EXPLICIT });
}
} finally {
ref.dispose();
}
}
private async accept() {
const modifiedModel = (this.entry as ChatEditingModifiedFileEntry).modifiedModel;
const content = modifiedModel.getValue();
await this.updateNotebook(VSBuffer.fromString(content), false);
}
async getNotebookSerializer() {
const info = await this.notebookService.withNotebookDataProvider(this.model.viewType);
return info.serializer;
}
private _originalModel?: Promise<NotebookTextModel>;
private async getOriginalModel(): Promise<NotebookTextModel> {
if (!this._originalModel) {
this._originalModel = this.originalModelRefFactory.getOrCreate(this.entry, this.model.viewType).then(ref => this._register(ref).object);
}
return this._originalModel;
}
private async updateNotebookModel(entry: IModifiedFileEntry, token: CancellationToken) {
const modifiedModelVersion = (entry as ChatEditingModifiedFileEntry).modifiedModel.getVersionId();
const currentModel = this.model;
const modelVersion = currentModel?.versionId ?? 0;
const modelWithChatEdits = await this.getModifiedModelForDiff(entry, token);
if (!modelWithChatEdits || token.isCancellationRequested || !currentModel) {
return;
}
const originalModel = await this.getOriginalModel();
// This is the total diff from the original model to the model with chat edits.
const cellDiffInfo = (await this.computeDiff(originalModel, modelWithChatEdits, token))?.cellDiffInfo;
// This is the diff from the current model to the model with chat edits.
const cellDiffInfoToApplyEdits = (await this.computeDiff(currentModel, modelWithChatEdits, token))?.cellDiffInfo;
const currentVersion = (entry as ChatEditingModifiedFileEntry).modifiedModel.getVersionId();
if (!cellDiffInfo || !cellDiffInfoToApplyEdits || token.isCancellationRequested || currentVersion !== modifiedModelVersion || modelVersion !== currentModel.versionId) {
return;
}
if (cellDiffInfoToApplyEdits.every(d => d.type === 'unchanged')) {
return;
}
// All edits from here on are from us.
this.isEditFromUs = true;
try {
const edits: ICellReplaceEdit[] = [];
const mappings = new Map<number, number>();
// First Delete.
const deletedIndexes: number[] = [];
cellDiffInfoToApplyEdits.reverse().forEach(diff => {
if (diff.type === 'delete') {
deletedIndexes.push(diff.originalCellIndex);
edits.push({
editType: CellEditType.Replace,
index: diff.originalCellIndex,
cells: [],
count: 1
});
}
});
if (edits.length) {
currentModel.applyEdits(edits, true, undefined, () => undefined, undefined, false);
edits.length = 0;
}
// Next insert.
cellDiffInfoToApplyEdits.reverse().forEach(diff => {
if (diff.type === 'modified' || diff.type === 'unchanged') {
mappings.set(diff.modifiedCellIndex, diff.originalCellIndex);
}
if (diff.type === 'insert') {
const originalIndex = mappings.get(diff.modifiedCellIndex - 1) ?? 0;
mappings.set(diff.modifiedCellIndex, originalIndex);
const cell = modelWithChatEdits.cells[diff.modifiedCellIndex];
const newCell: ICellDto2 =
{
source: cell.getValue(),
cellKind: cell.cellKind,
language: cell.language,
outputs: cell.outputs.map(output => output.asDto()),
mime: cell.mime,
metadata: cell.metadata,
collapseState: cell.collapseState,
internalMetadata: cell.internalMetadata
};
edits.push({
editType: CellEditType.Replace,
index: originalIndex + 1,
cells: [newCell],
count: 0
});
}
});
if (edits.length) {
currentModel.applyEdits(edits, true, undefined, () => undefined, undefined, false);
edits.length = 0;
}
// Finally update
cellDiffInfoToApplyEdits.forEach(diff => {
if (diff.type === 'modified') {
const cell = currentModel.cells[diff.originalCellIndex];
const textModel = cell.textModel;
if (textModel) {
const newText = modelWithChatEdits.cells[diff.modifiedCellIndex].getValue();
textModel.pushEditOperations(null, [
EditOperation.replace(textModel.getFullModelRange(), newText)
], () => null);
}
}
});
if (edits.length) {
currentModel.applyEdits(edits, true, undefined, () => undefined, undefined, false);
}
this._diffInfo.set({ cellDiff: cellDiffInfo, modelVersion: currentModel.versionId }, undefined);
}
finally {
this.isEditFromUs = false;
}
}
private previousUriOfModelForDiff?: URI;
private async getModifiedModelForDiff(entry: IModifiedFileEntry, token: CancellationToken): Promise<NotebookTextModel | undefined> {
const text = (entry as ChatEditingModifiedFileEntry).modifiedModel.getValue();
const bytes = VSBuffer.fromString(text);
const uri = entry.modifiedURI.with({ scheme: `NotebookChatEditorController.modifiedScheme${Date.now().toString()}` });
const stream = bufferToStream(bytes);
if (this.previousUriOfModelForDiff) {
this.notebookService.getNotebookTextModel(this.previousUriOfModelForDiff)?.dispose();
}
this.previousUriOfModelForDiff = uri;
try {
const model = await this.notebookService.createNotebookTextModel(this.model.viewType, uri, stream);
if (token.isCancellationRequested) {
model.dispose();
return;
}
this._register(model);
return model;
} catch (ex) {
this.logService.warn('NotebookChatEdit', `Failed to deserialize Notebook for ${uri.toString}, ${ex.message}`);
this.logService.debug('NotebookChatEdit', ex.toString());
return;
}
}
async computeDiff(original: NotebookTextModel, modified: NotebookTextModel, token: CancellationToken) {
const diffResult = await raceCancellation(this.notebookEditorWorkerService.computeDiff(original.uri, modified.uri), token);
if (!diffResult || token.isCancellationRequested) {
// after await the editor might be disposed.
return;
}
prettyChanges(original, modified, diffResult.cellsDiff);
return computeDiff(original, modified, diffResult);
}
}
@@ -75,7 +75,7 @@ export class EmptyCellEditorHintContribution extends EmptyTextEditorHintContribu
}
const activeEditor = getNotebookEditorFromEditorPane(this._editorService.activeEditorPane);
if (!activeEditor) {
if (!activeEditor || !activeEditor.isDisposed) {
return false;
}
@@ -7,12 +7,25 @@
padding: 12px 16px;
}
/** Cell delete higlight */
.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .view-zones .cell-inner-container {
background-color: var(--vscode-diffEditor-removedLineBackground);
padding: 8px 0;
margin-bottom: 16px;
}
/** Cell insert higlight */
.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.code-cell-row.nb-insertHighlight .cell-focus-indicator,
.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.markdown-cell-row.nb-insertHighlight {
background-color: var(--vscode-diffEditor-insertedLineBackground, var(--vscode-diffEditor-insertedTextBackground)) !important;
}
.notebookOverlay .cell .cell-statusbar-container .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.code-cell-row.nb-insertHighlight .cell-focus-indicator .cell-inner-container,
.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.code-cell-row.nb-insertHighlight .monaco-editor-background,
.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.code-cell-row.nb-insertHighlight .margin-view-overlays,
.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.code-cell-row.nb-insertHighlight .cell-statusbar-container {
background-color: var(--vscode-diffEditor-insertedLineBackground, var(--vscode-diffEditor-insertedTextBackground)) !important;
}
.monaco-workbench .notebookOverlay .view-zones .cell-editor-part {
outline: solid 1px var(--vscode-notebook-cellBorderColor);
@@ -0,0 +1,63 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.notebook-chat-editor-overlay-widget {
position: absolute;
/** Based on chat widget for regular editors **/
right: 28px;
/** Based on chat widget for regular editors **/
bottom: 23px;
/** In notebook.css we set this to 22px, we need to revert this to standards **/
line-height: 1.4em;
}
/** Copied from src/vs/workbench/contrib/chat/browser/media/chatEditorOverlay.css **/
/** Copied until we unify these, for now its separate **/
.notebook-chat-editor-overlay-widget {
padding: 0px;
color: var(--vscode-button-foreground);
background-color: var(--vscode-button-background);
border-radius: 5px;
border: 1px solid var(--vscode-contrastBorder);
display: flex;
align-items: center;
z-index: 10;
}
.notebook-chat-editor-overlay-widget .chat-editor-overlay-progress {
display: none;
padding: 0px 5px;
font-size: 12px;
}
.notebook-chat-editor-overlay-widget.busy .chat-editor-overlay-progress {
display: inherit;
}
.notebook-chat-editor-overlay-widget .action-item > .action-label {
padding: 5px;
font-size: 12px;
}
.notebook-chat-editor-overlay-widget .action-item:first-child > .action-label {
padding-left: 9px;
}
.notebook-chat-editor-overlay-widget .action-item:last-child > .action-label {
padding-right: 9px;
}
.notebook-chat-editor-overlay-widget.busy .chat-editor-overlay-progress .codicon,
.notebook-chat-editor-overlay-widget .action-item > .action-label.codicon {
color: var(--vscode-button-foreground);
}
.notebook-chat-editor-overlay-widget .action-item.disabled > .action-label.codicon::before,
.notebook-chat-editor-overlay-widget .action-item.disabled > .action-label.codicon,
.notebook-chat-editor-overlay-widget .action-item.disabled > .action-label,
.notebook-chat-editor-overlay-widget .action-item.disabled > .action-label:hover {
color: var(--vscode-button-foreground);
opacity: 0.7;
}
@@ -130,8 +130,11 @@ import { NotebookMultiDiffEditorInput } from './diff/notebookMultiDiffEditorInpu
import { getFormattedMetadataJSON } from '../common/model/notebookCellTextModel.js';
import { INotebookOutlineEntryFactory, NotebookOutlineEntryFactory } from './viewModel/notebookOutlineEntryFactory.js';
import { getFormattedNotebookMetadataJSON } from '../common/model/notebookMetadataTextModel.js';
import { INotebookOriginalModelReferenceFactory, NotebookChatEditorControllerContrib, NotebookOriginalModelReferenceFactory } from './notebookChatEditController.js';
import { NotebookChatEditorControllerContrib } from './chatEdit/notebookChatEditController.js';
import { registerNotebookContribution } from './notebookEditorExtensions.js';
import { INotebookOriginalModelReferenceFactory, NotebookOriginalModelReferenceFactory } from './chatEdit/notebookOriginalModelRefFactory.js';
import { INotebookModelSynchronizerFactory, NotebookModelSynchronizerFactory } from './chatEdit/notebookSynronizer.js';
import { INotebookOriginalCellModelFactory, OriginalNotebookCellModelFactory } from './chatEdit/notebookOriginalCellModelFactory.js';
/*--------------------------------------------------------------------------------------------- */
@@ -880,6 +883,8 @@ registerSingleton(INotebookOutlineEntryFactory, NotebookOutlineEntryFactory, Ins
registerNotebookContribution(NotebookChatEditorControllerContrib.ID, NotebookChatEditorControllerContrib);
registerSingleton(INotebookOriginalModelReferenceFactory, NotebookOriginalModelReferenceFactory, InstantiationType.Delayed);
registerSingleton(INotebookModelSynchronizerFactory, NotebookModelSynchronizerFactory, InstantiationType.Delayed);
registerSingleton(INotebookOriginalCellModelFactory, OriginalNotebookCellModelFactory, InstantiationType.Delayed);
const schemas: IJSONSchemaMap = {};
function isConfigurationPropertySchema(x: IConfigurationPropertySchema | { [path: string]: IConfigurationPropertySchema }): x is IConfigurationPropertySchema {
@@ -39,6 +39,9 @@ export class NotebookAccessibilityProvider extends Disposable implements IListAc
(last: executionUpdate[] | undefined, e: ICellExecutionStateChangedEvent | IExecutionStateChangedEvent) => this.mergeEvents(last, e),
100
)((updates: executionUpdate[]) => {
if (!updates.length) {
return;
}
const viewModel = this.viewModel();
if (viewModel) {
for (const update of updates) {
@@ -52,7 +55,7 @@ export class NotebookAccessibilityProvider extends Disposable implements IListAc
if (this.shouldReadCellOutputs(lastUpdate.state)) {
const cell = viewModel.getCellByHandle(lastUpdate.cellHandle);
if (cell && cell.outputsViewModels.length) {
const text = getAllOutputsText(viewModel.notebookDocument, cell);
const text = getAllOutputsText(viewModel.notebookDocument, cell, true);
alert(text);
}
}
@@ -1,939 +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 { isEqual } from '../../../../base/common/resources.js';
import { AsyncReferenceCollection, Disposable, DisposableStore, dispose, IReference, ReferenceCollection, toDisposable } from '../../../../base/common/lifecycle.js';
import { autorun, autorunWithStore, derived, observableFromEvent, observableValue } from '../../../../base/common/observable.js';
import { IChatEditingService, WorkingSetEntryState, IModifiedFileEntry, ChatEditingSessionState } from '../../chat/common/chatEditingService.js';
import { INotebookService } from '../common/notebookService.js';
import { bufferToStream, VSBuffer } from '../../../../base/common/buffer.js';
import { NotebookTextModel } from '../common/model/notebookTextModel.js';
import { INotebookEditor, INotebookEditorContribution } from './notebookBrowser.js';
import { createDecorator, IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
import { raceCancellation, ThrottledDelayer } from '../../../../base/common/async.js';
import { CellDiffInfo, computeDiff, prettyChanges } from './diff/notebookDiffViewModel.js';
import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js';
import { INotebookEditorWorkerService } from '../common/services/notebookWorkerService.js';
import { ChatEditingModifiedFileEntry } from '../../chat/browser/chatEditing/chatEditingModifiedFileEntry.js';
import { CellEditType, CellKind, CellUri, ICellDto2, ICellReplaceEdit, NotebookData, NotebookSetting } from '../common/notebookCommon.js';
import { URI } from '../../../../base/common/uri.js';
import { Emitter } from '../../../../base/common/event.js';
import { ICodeEditor, IViewZone } from '../../../../editor/browser/editorBrowser.js';
import { IModelService } from '../../../../editor/common/services/model.js';
import { IEditorWorkerService } from '../../../../editor/common/services/editorWorker.js';
import { ILanguageService } from '../../../../editor/common/languages/language.js';
import { EditorOption } from '../../../../editor/common/config/editorOptions.js';
import { themeColorFromId } from '../../../../base/common/themables.js';
import { RenderOptions, LineSource, renderLines } from '../../../../editor/browser/widget/diffEditor/components/diffEditorViewZones/renderLines.js';
import { diffAddDecoration, diffWholeLineAddDecoration, diffDeleteDecoration } from '../../../../editor/browser/widget/diffEditor/registrations.contribution.js';
import { IDocumentDiff } from '../../../../editor/common/diff/documentDiffProvider.js';
import { ITextModel, TrackedRangeStickiness, MinimapPosition, IModelDeltaDecoration, OverviewRulerLane } from '../../../../editor/common/model.js';
import { ModelDecorationOptions } from '../../../../editor/common/model/textModel.js';
import { InlineDecoration, InlineDecorationType } from '../../../../editor/common/viewModel.js';
import { overviewRulerModifiedForeground, minimapGutterModifiedBackground, overviewRulerAddedForeground, minimapGutterAddedBackground, overviewRulerDeletedForeground, minimapGutterDeletedBackground } from '../../scm/browser/dirtydiffDecorator.js';
import { Range } from '../../../../editor/common/core/range.js';
import { NotebookCellTextModel } from '../common/model/notebookCellTextModel.js';
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
import { EditOperation } from '../../../../editor/common/core/editOperation.js';
import { INotebookLoggingService } from '../common/notebookLoggingService.js';
import { TextEdit } from '../../../../editor/common/core/textEdit.js';
import { Position } from '../../../../editor/common/core/position.js';
import { DetailedLineRangeMapping, RangeMapping } from '../../../../editor/common/diff/rangeMapping.js';
import { tokenizeToString } from '../../../../editor/common/languages/textToHtmlTokenizer.js';
import * as DOM from '../../../../base/browser/dom.js';
import { createTrustedTypesPolicy } from '../../../../base/browser/trustedTypes.js';
import { splitLines } from '../../../../base/common/strings.js';
import { DefaultLineHeight } from './diff/diffElementViewModel.js';
import { filter } from '../../../../base/common/objects.js';
import { INotebookEditorModelResolverService } from '../common/notebookEditorModelResolverService.js';
import { SaveReason } from '../../../common/editor.js';
export const INotebookOriginalModelReferenceFactory = createDecorator<INotebookOriginalModelReferenceFactory>('INotebookOriginalModelReferenceFactory');
export interface INotebookOriginalModelReferenceFactory {
readonly _serviceBrand: undefined;
getOrCreate(fileEntry: IModifiedFileEntry, viewType: string): Promise<IReference<NotebookTextModel>>;
}
export class NotebookChatEditorControllerContrib extends Disposable implements INotebookEditorContribution {
public static readonly ID: string = 'workbench.notebook.chatEditorController';
readonly _serviceBrand: undefined;
constructor(
notebookEditor: INotebookEditor,
@IInstantiationService instantiationService: IInstantiationService,
@IConfigurationService configurationService: IConfigurationService,
) {
super();
if (configurationService.getValue<boolean>('notebook.experimental.chatEdits')) {
this._register(instantiationService.createInstance(NotebookChatEditorController, notebookEditor));
}
}
}
class NotebookChatEditorController extends Disposable {
private readonly deletedCellOverlayer: NotebookDeletedCellOverlayer;
constructor(
private readonly notebookEditor: INotebookEditor,
@IChatEditingService private readonly _chatEditingService: IChatEditingService,
@INotebookOriginalModelReferenceFactory private readonly originalModelRefFactory: INotebookOriginalModelReferenceFactory,
@IInstantiationService private readonly instantiationService: IInstantiationService,
) {
super();
this.deletedCellOverlayer = this._register(instantiationService.createInstance(NotebookDeletedCellOverlayer, notebookEditor));
const notebookModel = observableFromEvent(this.notebookEditor.onDidChangeModel, e => e);
const entryObs = observableValue<IModifiedFileEntry | undefined>('fileentry', undefined);
const notebookDiff = observableValue<{ cellDiff: CellDiffInfo[]; modelVersion: number } | undefined>('cellDiffInfo', undefined);
const originalModel = observableValue<NotebookTextModel | undefined>('originalModel', undefined);
this._register(toDisposable(() => {
disposeDecorators();
}));
this._register(autorun(r => {
const session = this._chatEditingService.currentEditingSessionObs.read(r);
const model = notebookModel.read(r);
if (!model || !session) {
return;
}
const entry = session.entries.read(r).find(e => isEqual(e.modifiedURI, model.uri));
if (!entry || entry.state.read(r) !== WorkingSetEntryState.Modified) {
disposeDecorators();
return;
}
// If we have a new entry for the file, then clear old decorators.
// User could be cycling through different edit sessions (Undo Last Edit / Redo Last Edit).
if (entryObs.read(r) && entryObs.read(r) !== entry) {
disposeDecorators();
}
entryObs.set(entry, undefined);
}));
this._register(autorunWithStore(async (r, store) => {
const entry = entryObs.read(r);
const model = notebookModel.read(r);
if (!entry || !model) {
return;
}
const notebookSynchronizer = store.add(this.instantiationService.createInstance(NotebookModelSynchronizer, this.notebookEditor, entry, model.viewType));
notebookDiff.set(undefined, undefined);
await notebookSynchronizer.createSnapshot();
store.add(notebookSynchronizer.onDidUpdateNotebookModel(e => {
notebookDiff.set(e, undefined);
}));
store.add(notebookSynchronizer.onDidRevert(e => {
if (e) {
disposeDecorators();
this.deletedCellOverlayer.clear();
}
}));
const result = this._register(await this.originalModelRefFactory.getOrCreate(entry, model.viewType));
originalModel.set(result.object, undefined);
}));
const onDidChangeVisibleRanges = observableFromEvent(this.notebookEditor.onDidChangeVisibleRanges, () => this.notebookEditor.visibleRanges);
const decorators = new Map<NotebookCellTextModel, NotebookCellDiffDecorator>();
const disposeDecorators = () => {
dispose(Array.from(decorators.values()));
decorators.clear();
};
this._register(autorun(r => {
const entry = entryObs.read(r);
const diffInfo = notebookDiff.read(r);
const modified = notebookModel.read(r);
const original = originalModel.read(r);
onDidChangeVisibleRanges.read(r);
if (!entry || !modified || !original || !diffInfo || diffInfo.modelVersion !== modified.versionId) {
return;
}
diffInfo.cellDiff.forEach((diff) => {
if (diff.type === 'modified' || diff.type === 'insert') {
const modifiedCell = modified.cells[diff.modifiedCellIndex];
const originalCellValue = diff.type === 'modified' ? original.cells[diff.originalCellIndex].getValue() : undefined;
const editor = this.notebookEditor.codeEditors.find(([vm,]) => vm.handle === modifiedCell.handle)?.[1];
if (editor && decorators.get(modifiedCell)?.editor !== editor) {
decorators.get(modifiedCell)?.dispose();
const decorator = this.instantiationService.createInstance(NotebookCellDiffDecorator, editor, originalCellValue, modifiedCell.cellKind);
decorators.set(modifiedCell, decorator);
this._register(editor.onDidDispose(() => {
decorator.dispose();
if (decorators.get(modifiedCell) === decorator) {
decorators.set(modifiedCell, decorator);
}
}));
}
}
});
}));
this._register(autorun(r => {
const entry = entryObs.read(r);
const diffInfo = notebookDiff.read(r);
const modified = notebookModel.read(r);
const original = originalModel.read(r);
if (!entry || !modified || !original || (diffInfo && diffInfo.modelVersion !== modified.versionId)) {
return;
}
if (!diffInfo) {
// User reverted the changes, hence original === modified.
this.deletedCellOverlayer.clear();
return;
}
this.deletedCellOverlayer.createNecessaryOverlays(diffInfo.cellDiff, original);
}));
}
}
class NotebookCellDiffDecorator extends DisposableStore {
private readonly _decorations = this.editor.createDecorationsCollection();
private _viewZones: string[] = [];
private readonly throttledDecorator = new ThrottledDelayer(100);
constructor(
public readonly editor: ICodeEditor,
private readonly originalCellValue: string | undefined,
private readonly cellKind: CellKind,
@IChatEditingService private readonly _chatEditingService: IChatEditingService,
@IModelService private readonly modelService: IModelService,
@IEditorWorkerService private readonly _editorWorkerService: IEditorWorkerService,
@ILanguageService private readonly _languageService: ILanguageService,
) {
super();
this.add(this.editor.onDidChangeModel(() => this.update()));
this.add(this.editor.onDidChangeConfiguration((e) => {
if (e.hasChanged(EditorOption.fontInfo) || e.hasChanged(EditorOption.lineHeight)) {
this.update();
}
}));
const shouldBeReadOnly = derived(this, r => {
const value = this._chatEditingService.currentEditingSessionObs.read(r);
if (!value || value.state.read(r) !== ChatEditingSessionState.StreamingEdits) {
return false;
}
return value.entries.read(r).some(e => isEqual(e.modifiedURI, this.editor.getModel()?.uri));
});
let actualReadonly: boolean | undefined;
let actualDeco: 'off' | 'editable' | 'on' | undefined;
this.add(autorun(r => {
const value = shouldBeReadOnly.read(r);
if (value) {
actualReadonly ??= this.editor.getOption(EditorOption.readOnly);
actualDeco ??= this.editor.getOption(EditorOption.renderValidationDecorations);
this.editor.updateOptions({
readOnly: true,
renderValidationDecorations: 'off'
});
} else {
if (actualReadonly !== undefined && actualDeco !== undefined) {
this.editor.updateOptions({
readOnly: actualReadonly,
renderValidationDecorations: actualDeco
});
actualReadonly = undefined;
actualDeco = undefined;
}
}
}));
this.update();
}
override dispose(): void {
this._clearRendering();
super.dispose();
}
public update(): void {
this.throttledDecorator.trigger(() => this._updateImpl());
}
private async _updateImpl() {
if (this.isDisposed) {
return;
}
if (!this.editor.hasModel()) {
this._clearRendering();
return;
}
if (this.editor.getOption(EditorOption.inDiffEditor)) {
this._clearRendering();
return;
}
const model = this.editor.getModel();
if (!model) {
this._clearRendering();
return;
}
const version = model.getVersionId();
const originalModel = this.getOrCreateOriginalModel();
const diff = originalModel ? await this.computeDiff() : undefined;
if (this.isDisposed) {
return;
}
if ((originalModel && !diff) || model !== this.editor.getModel() || this.editor.getModel()?.getVersionId() !== version) {
this._clearRendering();
}
if (diff && originalModel) {
this._updateWithDiff(originalModel, diff);
} else {
const edit = TextEdit.insert(new Position(0, 0), model.getValue());
const rangeMapping = RangeMapping.fromEdit(edit);
const insertDiff: IDocumentDiff = {
identical: false,
moves: [],
quitEarly: false,
changes: [DetailedLineRangeMapping.fromRangeMappings(rangeMapping)],
};
this._updateWithDiff(undefined, insertDiff);
}
}
private _clearRendering() {
this.editor.changeViewZones((viewZoneChangeAccessor) => {
for (const id of this._viewZones) {
viewZoneChangeAccessor.removeZone(id);
}
});
this._viewZones = [];
this._decorations.clear();
}
private _originalModel?: ITextModel;
private getOrCreateOriginalModel() {
if (this._originalModel) {
return this._originalModel;
}
if (!this.originalCellValue) {
return;
}
const model = this.editor.getModel();
if (!model) {
return;
}
const cellUri = model.uri;
const languageId = model.getLanguageId();
const scheme = `${CellUri.scheme}-chat-edit`;
const originalCellUri = URI.from({ scheme, fragment: cellUri.fragment, path: cellUri.path });
const languageSelection = this._languageService.getLanguageIdByLanguageName(languageId) ? this._languageService.createById(languageId) : this.cellKind === CellKind.Markup ? this._languageService.createById('markdown') : null;
return this._originalModel = this.add(this.modelService.createModel(this.originalCellValue, languageSelection, originalCellUri));
}
private async computeDiff() {
const model = this.editor.getModel();
if (!model) {
return;
}
const originalModel = this.getOrCreateOriginalModel();
if (!originalModel) {
return;
}
return this._editorWorkerService.computeDiff(
originalModel.uri,
model.uri,
{ computeMoves: true, ignoreTrimWhitespace: false, maxComputationTimeMs: Number.MAX_SAFE_INTEGER },
'advanced'
);
}
private _updateWithDiff(originalModel: ITextModel | undefined, diff: IDocumentDiff): void {
const chatDiffAddDecoration = ModelDecorationOptions.createDynamic({
...diffAddDecoration,
stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges
});
const chatDiffWholeLineAddDecoration = ModelDecorationOptions.createDynamic({
...diffWholeLineAddDecoration,
stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,
});
const createOverviewDecoration = (overviewRulerColor: string, minimapColor: string) => {
return ModelDecorationOptions.createDynamic({
description: 'chat-editing-decoration',
overviewRuler: { color: themeColorFromId(overviewRulerColor), position: OverviewRulerLane.Left },
minimap: { color: themeColorFromId(minimapColor), position: MinimapPosition.Gutter },
});
};
const modifiedDecoration = createOverviewDecoration(overviewRulerModifiedForeground, minimapGutterModifiedBackground);
const addedDecoration = createOverviewDecoration(overviewRulerAddedForeground, minimapGutterAddedBackground);
const deletedDecoration = createOverviewDecoration(overviewRulerDeletedForeground, minimapGutterDeletedBackground);
this.editor.changeViewZones((viewZoneChangeAccessor) => {
for (const id of this._viewZones) {
viewZoneChangeAccessor.removeZone(id);
}
this._viewZones = [];
const modifiedDecorations: IModelDeltaDecoration[] = [];
const mightContainNonBasicASCII = originalModel?.mightContainNonBasicASCII();
const mightContainRTL = originalModel?.mightContainRTL();
const renderOptions = RenderOptions.fromEditor(this.editor);
for (const diffEntry of diff.changes) {
const originalRange = diffEntry.original;
if (originalModel) {
originalModel.tokenization.forceTokenization(Math.max(1, originalRange.endLineNumberExclusive - 1));
}
const source = new LineSource(
(originalRange.length && originalModel) ? originalRange.mapToLineArray(l => originalModel.tokenization.getLineTokens(l)) : [],
[],
mightContainNonBasicASCII,
mightContainRTL,
);
const decorations: InlineDecoration[] = [];
for (const i of diffEntry.innerChanges || []) {
decorations.push(new InlineDecoration(
i.originalRange.delta(-(diffEntry.original.startLineNumber - 1)),
diffDeleteDecoration.className!,
InlineDecorationType.Regular
));
modifiedDecorations.push({
range: i.modifiedRange, options: chatDiffAddDecoration
});
}
if (!diffEntry.modified.isEmpty) {
modifiedDecorations.push({
range: diffEntry.modified.toInclusiveRange()!, options: chatDiffWholeLineAddDecoration
});
}
if (diffEntry.original.isEmpty) {
// insertion
modifiedDecorations.push({
range: diffEntry.modified.toInclusiveRange()!,
options: addedDecoration
});
} else if (diffEntry.modified.isEmpty) {
// deletion
modifiedDecorations.push({
range: new Range(diffEntry.modified.startLineNumber - 1, 1, diffEntry.modified.startLineNumber, 1),
options: deletedDecoration
});
} else {
// modification
modifiedDecorations.push({
range: diffEntry.modified.toInclusiveRange()!,
options: modifiedDecoration
});
}
const domNode = document.createElement('div');
domNode.className = 'chat-editing-original-zone view-lines line-delete monaco-mouse-cursor-text';
const result = renderLines(source, renderOptions, decorations, domNode);
const isCreatedContent = decorations.length === 1 && decorations[0].range.isEmpty() && decorations[0].range.startLineNumber === 1;
if (!isCreatedContent) {
const viewZoneData: IViewZone = {
afterLineNumber: diffEntry.modified.startLineNumber - 1,
heightInLines: result.heightInLines,
domNode,
ordinal: 50000 + 2 // more than https://github.com/microsoft/vscode/blob/bf52a5cfb2c75a7327c9adeaefbddc06d529dcad/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts#L42
};
this._viewZones.push(viewZoneChangeAccessor.addZone(viewZoneData));
}
}
this._decorations.set(modifiedDecorations);
});
}
}
class NotebookModelSynchronizer extends Disposable {
private readonly throttledUpdateNotebookModel = new ThrottledDelayer(200);
private readonly _onDidUpdateNotebookModel = this._register(new Emitter<{ cellDiff: CellDiffInfo[]; modelVersion: number }>);
public readonly onDidUpdateNotebookModel = this._onDidUpdateNotebookModel.event;
private readonly _onDidRevert = this._register(new Emitter<boolean>());
public readonly onDidRevert = this._onDidRevert.event;
private snapshot?: { bytes: VSBuffer; dirty: boolean };
private isEditFromUs: boolean = false;
constructor(
private readonly notebookEditor: INotebookEditor,
public readonly entry: IModifiedFileEntry,
private readonly viewType: string,
@INotebookService private readonly notebookService: INotebookService,
@INotebookLoggingService private readonly logService: INotebookLoggingService,
@IConfigurationService private readonly configurationService: IConfigurationService,
@INotebookEditorWorkerService private readonly notebookEditorWorkerService: INotebookEditorWorkerService,
@INotebookEditorModelResolverService private readonly notebookModelResolverService: INotebookEditorModelResolverService,
) {
super();
const cancellationTokenStore = this._register(new DisposableStore());
let cancellationToken = cancellationTokenStore.add(new CancellationTokenSource());
const updateNotebookModel = (entry: IModifiedFileEntry, viewType: string, token: CancellationToken) => {
this.throttledUpdateNotebookModel.trigger(() => this.updateNotebookModel(entry, viewType, token));
};
const modelObs = observableFromEvent(notebookEditor.onDidChangeModel, e => e);
const modifiedModel = (entry as ChatEditingModifiedFileEntry).modifiedModel;
this._register(modifiedModel.onDidChangeContent(() => {
cancellationTokenStore.clear();
if (!modifiedModel.isDisposed() && !entry.originalModel.isDisposed() && modifiedModel.getValue() === entry.originalModel.getValue()) {
this.revert();
return;
}
cancellationToken = cancellationTokenStore.add(new CancellationTokenSource());
updateNotebookModel(entry, viewType, cancellationToken.token);
}));
this._register(autorunWithStore((r, store) => {
const model = modelObs.read(r);
if (model) {
store.add(model.onDidChangeContent(() => {
// Track changes from the user.
if (!this.isEditFromUs && this.snapshot) {
this.snapshot.dirty = true;
}
}));
}
}));
updateNotebookModel(entry, viewType, cancellationToken.token);
}
public async createSnapshot() {
const model = this.notebookEditor.textModel;
if (!model) {
return;
}
const [serializer, ref] = await Promise.all([
this.getNotebookSerializer(),
this.notebookModelResolverService.resolve(this.notebookEditor.textModel.uri)
]);
try {
const data: NotebookData = {
metadata: filter(model.metadata, key => !serializer.options.transientDocumentMetadata[key]),
cells: [],
};
let outputSize = 0;
for (const cell of model.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<number>(NotebookSetting.outputBackupSizeLimit) * 1024;
if (outputSizeLimit > 0) {
cell.outputs.forEach(output => {
output.outputs.forEach(item => {
outputSize += item.data.byteLength;
});
});
if (outputSize > outputSizeLimit) {
return;
}
}
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);
this.snapshot = { bytes, dirty: ref.object.isDirty() };
} finally {
ref.dispose();
}
}
public async revert(): Promise<void> {
if (!this.snapshot || !this.notebookEditor.textModel) {
return;
}
const ref = await this.notebookModelResolverService.resolve(this.notebookEditor.textModel.uri);
try {
const serializer = await this.getNotebookSerializer();
const data = await serializer.dataToNotebook(this.snapshot.bytes);
this.notebookEditor.textModel.reset(data.cells, data.metadata, serializer.options);
if (!this.snapshot.dirty) {
// save the file after discarding so that the dirty indicator goes away
// and so that an intermediate saved state gets reverted
// await this.notebookEditor.textModel.save({ reason: SaveReason.EXPLICIT });
await ref.object.save({ reason: SaveReason.EXPLICIT });
}
this._onDidRevert.fire(true);
} finally {
ref.dispose();
}
}
async getNotebookSerializer() {
const info = await this.notebookService.withNotebookDataProvider(this.viewType);
return info.serializer;
}
private async updateNotebookModel(entry: IModifiedFileEntry, viewType: string, token: CancellationToken) {
const modifiedModelVersion = (entry as ChatEditingModifiedFileEntry).modifiedModel.getVersionId();
const original = this.notebookEditor.textModel;
const originalModelVersion = original?.versionId ?? 0;
const model = await this.getModifiedModelForDiff(entry, viewType, token);
if (!model || token.isCancellationRequested || !original) {
return;
}
const cellDiffInfo = (await this.computeDiff(original, model, token))?.cellDiffInfo;
const currentVersion = (entry as ChatEditingModifiedFileEntry).modifiedModel.getVersionId();
if (!cellDiffInfo || token.isCancellationRequested || currentVersion !== modifiedModelVersion || originalModelVersion !== original.versionId) {
return;
}
if (cellDiffInfo.every(d => d.type === 'unchanged')) {
return;
}
// All edits from here on are from us.
this.isEditFromUs = true;
try {
const edits: ICellReplaceEdit[] = [];
const mappings = new Map<number, number>();
// First Delete.
const deletedIndexes: number[] = [];
cellDiffInfo.reverse().forEach(diff => {
if (diff.type === 'delete') {
deletedIndexes.push(diff.originalCellIndex);
edits.push({
editType: CellEditType.Replace,
index: diff.originalCellIndex,
cells: [],
count: 1
});
}
});
if (edits.length) {
original.applyEdits(edits, true, undefined, () => undefined, undefined, false);
edits.length = 0;
}
// Next insert.
cellDiffInfo.reverse().forEach(diff => {
if (diff.type === 'modified' || diff.type === 'unchanged') {
mappings.set(diff.modifiedCellIndex, diff.originalCellIndex);
}
if (diff.type === 'insert') {
const originalIndex = mappings.get(diff.modifiedCellIndex - 1) ?? 0;
mappings.set(diff.modifiedCellIndex, originalIndex);
const cell = model.cells[diff.modifiedCellIndex];
const newCell: ICellDto2 =
{
source: cell.getValue(),
cellKind: cell.cellKind,
language: cell.language,
outputs: cell.outputs.map(output => output.asDto()),
mime: cell.mime,
metadata: cell.metadata,
collapseState: cell.collapseState,
internalMetadata: cell.internalMetadata
};
edits.push({
editType: CellEditType.Replace,
index: originalIndex + 1,
cells: [newCell],
count: 0
});
}
});
if (edits.length) {
original.applyEdits(edits, true, undefined, () => undefined, undefined, false);
edits.length = 0;
}
// Finally update
cellDiffInfo.forEach(diff => {
if (diff.type === 'modified') {
const cell = original.cells[diff.originalCellIndex];
const textModel = cell.textModel;
if (textModel) {
const newText = model.cells[diff.modifiedCellIndex].getValue();
textModel.pushEditOperations(null, [
EditOperation.replace(textModel.getFullModelRange(), newText)
], () => null);
}
}
});
if (edits.length) {
original.applyEdits(edits, true, undefined, () => undefined, undefined, false);
}
this._onDidRevert.fire(false);
this._onDidUpdateNotebookModel.fire({ cellDiff: cellDiffInfo, modelVersion: original.versionId });
}
finally {
this.isEditFromUs = false;
}
}
private previousUriOfModelForDiff?: URI;
private async getModifiedModelForDiff(entry: IModifiedFileEntry, viewType: string, token: CancellationToken): Promise<NotebookTextModel | undefined> {
const text = (entry as ChatEditingModifiedFileEntry).modifiedModel.getValue();
const bytes = VSBuffer.fromString(text);
const uri = entry.modifiedURI.with({ scheme: `NotebookChatEditorController.modifiedScheme${Date.now().toString()}` });
const stream = bufferToStream(bytes);
if (this.previousUriOfModelForDiff) {
this.notebookService.getNotebookTextModel(this.previousUriOfModelForDiff)?.dispose();
}
this.previousUriOfModelForDiff = uri;
try {
const model = await this.notebookService.createNotebookTextModel(viewType, uri, stream);
if (token.isCancellationRequested) {
model.dispose();
return;
}
this._register(model);
return model;
} catch (ex) {
this.logService.warn('NotebookChatEdit', `Failed to deserialize Notebook for ${uri.toString}, ${ex.message}`);
this.logService.debug('NotebookChatEdit', ex.toString());
return;
}
}
async computeDiff(original: NotebookTextModel, modified: NotebookTextModel, token: CancellationToken) {
const diffResult = await raceCancellation(this.notebookEditorWorkerService.computeDiff(original.uri, modified.uri), token);
if (!diffResult || token.isCancellationRequested) {
// after await the editor might be disposed.
return;
}
prettyChanges(original, modified, diffResult.cellsDiff);
return computeDiff(original, modified, diffResult);
}
}
const ttPolicy = createTrustedTypesPolicy('notebookChatEditController', { createHTML: value => value });
class NotebookDeletedCellOverlayer extends Disposable {
private readonly zoneRemover = this._register(new DisposableStore());
private readonly createdViewZones = new Map<number, string>();
constructor(
private readonly _notebookEditor: INotebookEditor,
@ILanguageService private readonly languageService: ILanguageService,
) {
super();
}
public createNecessaryOverlays(diffInfo: CellDiffInfo[], original: NotebookTextModel): void {
this.clear();
let currentIndex = 0;
const deletedCellsToRender: { cells: NotebookCellTextModel[]; index: number } = { cells: [], index: 0 };
diffInfo.forEach(diff => {
if (diff.type === 'delete') {
const deletedCell = original.cells[diff.originalCellIndex];
if (deletedCell) {
deletedCellsToRender.cells.push(deletedCell);
deletedCellsToRender.index = currentIndex;
}
} else {
if (deletedCellsToRender.cells.length) {
this._createWidget(deletedCellsToRender.index + 1, deletedCellsToRender.cells);
deletedCellsToRender.cells.length = 0;
}
currentIndex = diff.modifiedCellIndex;
}
});
if (deletedCellsToRender.cells.length) {
this._createWidget(deletedCellsToRender.index + 1, deletedCellsToRender.cells);
}
}
public clear() {
this.zoneRemover.clear();
}
private _createWidget(index: number, cells: NotebookCellTextModel[]) {
this._createWidgetImpl(index, cells);
}
private async _createWidgetImpl(index: number, cells: NotebookCellTextModel[]) {
const rootContainer = document.createElement('div');
const widgets = cells.map(cell => new NotebookDeletedCellWidget(this._notebookEditor, cell.getValue(), cell.language, rootContainer, this.languageService));
const heights = await Promise.all(widgets.map(w => w.render()));
const totalHeight = heights.reduce<number>((prev, curr) => prev + curr, 0);
this._notebookEditor.changeViewZones(accessor => {
const notebookViewZone = {
afterModelPosition: index,
heightInPx: totalHeight,
domNode: rootContainer
};
const id = accessor.addZone(notebookViewZone);
accessor.layoutZone(id);
this.createdViewZones.set(index, id);
this.zoneRemover.add(toDisposable(() => {
if (this.createdViewZones.get(index) === id) {
this.createdViewZones.delete(index);
}
if (!this._notebookEditor.isDisposed) {
this._notebookEditor.changeViewZones(accessor => {
accessor.removeZone(id);
dispose(widgets);
});
}
}));
});
}
}
export class OriginalNotebookModelReferenceCollection extends ReferenceCollection<Promise<NotebookTextModel>> {
private readonly modelsToDispose = new Set<string>();
constructor(@INotebookService private readonly notebookService: INotebookService) {
super();
}
protected override async createReferencedObject(key: string, fileEntry: IModifiedFileEntry, viewType: string): Promise<NotebookTextModel> {
this.modelsToDispose.delete(key);
const uri = fileEntry.originalURI;
const model = this.notebookService.getNotebookTextModel(uri);
if (model) {
return model;
}
const bytes = VSBuffer.fromString(fileEntry.originalModel.getValue());
const stream = bufferToStream(bytes);
return this.notebookService.createNotebookTextModel(viewType, uri, stream);
}
protected override destroyReferencedObject(key: string, modelPromise: Promise<NotebookTextModel>): void {
this.modelsToDispose.add(key);
(async () => {
try {
const model = await modelPromise;
if (!this.modelsToDispose.has(key)) {
// return if model has been acquired again meanwhile
return;
}
// Finally we can dispose the model
model.dispose();
} catch (error) {
// ignore
} finally {
this.modelsToDispose.delete(key); // Untrack as being disposed
}
})();
}
}
class NotebookDeletedCellWidget extends Disposable {
private readonly container: HTMLElement;
constructor(
private readonly _notebookEditor: INotebookEditor,
// private readonly _index: number,
private readonly code: string,
private readonly language: string,
container: HTMLElement,
@ILanguageService private readonly languageService: ILanguageService,
) {
super();
this.container = DOM.append(container, document.createElement('div'));
this._register(toDisposable(() => {
container.removeChild(this.container);
}));
}
public async render() {
const code = this.code;
const languageId = this.language;
const codeHtml = await tokenizeToString(this.languageService, code, languageId);
// const colorMap = this.getDefaultColorMap();
const fontInfo = this._notebookEditor.getBaseCellEditorOptions(languageId).value;
const fontFamilyVar = '--notebook-editor-font-family';
const fontSizeVar = '--notebook-editor-font-size';
const fontWeightVar = '--notebook-editor-font-weight';
// If we have any editors, then use left layout of one of those.
const editor = this._notebookEditor.codeEditors.map(c => c[1]).find(c => c);
const layoutInfo = editor?.getOptions().get(EditorOption.layoutInfo);
const style = ``
+ `font-family: var(${fontFamilyVar});`
+ `font-weight: var(${fontWeightVar});`
+ `font-size: var(${fontSizeVar});`
+ fontInfo.lineHeight ? `line-height: ${fontInfo.lineHeight}px;` : ''
+ layoutInfo?.contentLeft ? `margin-left: ${layoutInfo}px;` : ''
+ `white-space: pre;`;
const rootContainer = this.container;
rootContainer.classList.add('code-cell-row');
const container = DOM.append(rootContainer, DOM.$('.cell-inner-container'));
const focusIndicatorLeft = DOM.append(container, DOM.$('.cell-focus-indicator.cell-focus-indicator-side.cell-focus-indicator-left'));
const cellContainer = DOM.append(container, DOM.$('.cell.code'));
DOM.append(focusIndicatorLeft, DOM.$('div.execution-count-label'));
const editorPart = DOM.append(cellContainer, DOM.$('.cell-editor-part'));
let editorContainer = DOM.append(editorPart, DOM.$('.cell-editor-container'));
editorContainer = DOM.append(editorContainer, DOM.$('.code', { style }));
if (fontInfo.fontFamily) {
editorContainer.style.setProperty(fontFamilyVar, fontInfo.fontFamily);
}
if (fontInfo.fontSize) {
editorContainer.style.setProperty(fontSizeVar, `${fontInfo.fontSize}px`);
}
if (fontInfo.fontWeight) {
editorContainer.style.setProperty(fontWeightVar, fontInfo.fontWeight);
}
editorContainer.innerHTML = (ttPolicy?.createHTML(codeHtml) || codeHtml) as string;
const lineCount = splitLines(code).length;
const height = (lineCount * (fontInfo.lineHeight || DefaultLineHeight)) + 12 + 12; // We have 12px top and bottom in generated code HTML;
const totalHeight = height + 16;
return totalHeight;
}
}
export class NotebookOriginalModelReferenceFactory implements INotebookOriginalModelReferenceFactory {
readonly _serviceBrand: undefined;
private _resourceModelCollection: OriginalNotebookModelReferenceCollection & ReferenceCollection<Promise<NotebookTextModel>> /* TS Fail */ | undefined = undefined;
private get resourceModelCollection() {
if (!this._resourceModelCollection) {
this._resourceModelCollection = this.instantiationService.createInstance(OriginalNotebookModelReferenceCollection);
}
return this._resourceModelCollection;
}
private _asyncModelCollection: AsyncReferenceCollection<NotebookTextModel> | undefined = undefined;
private get asyncModelCollection() {
if (!this._asyncModelCollection) {
this._asyncModelCollection = new AsyncReferenceCollection(this.resourceModelCollection);
}
return this._asyncModelCollection;
}
constructor(@IInstantiationService private readonly instantiationService: IInstantiationService) {
}
getOrCreate(fileEntry: IModifiedFileEntry, viewType: string): Promise<IReference<NotebookTextModel>> {
return this.asyncModelCollection.acquire(fileEntry.originalURI.toString(), fileEntry, viewType);
}
}
@@ -18,6 +18,7 @@ import './media/notebookEditorStickyScroll.css';
import './media/notebookKernelActionViewItem.css';
import './media/notebookOutline.css';
import './media/notebookChatEditController.css';
import './media/notebookChatEditorOverlay.css';
import * as DOM from '../../../../base/browser/dom.js';
import { IMouseWheelEvent, StandardMouseEvent } from '../../../../base/browser/mouseEvent.js';
import { IListContextMenuEvent } from '../../../../base/browser/ui/list/list.js';
@@ -15,7 +15,7 @@ interface Error {
stack?: string;
}
export function getAllOutputsText(notebook: NotebookTextModel, viewCell: ICellViewModel): string {
export function getAllOutputsText(notebook: NotebookTextModel, viewCell: ICellViewModel, shortErrors: boolean = false): string {
const outputText: string[] = [];
for (let i = 0; i < viewCell.outputsViewModels.length; i++) {
const outputViewModel = viewCell.outputsViewModels[i];
@@ -40,7 +40,7 @@ export function getAllOutputsText(notebook: NotebookTextModel, viewCell: ICellVi
i += count - 1;
}
} else {
text = getOutputText(mimeType, buffer);
text = getOutputText(mimeType, buffer, shortErrors);
}
outputText.push(text);
@@ -80,7 +80,7 @@ export function getOutputStreamText(output: ICellOutputViewModel): { text: strin
const decoder = new TextDecoder();
export function getOutputText(mimeType: string, buffer: IOutputItemDto) {
export function getOutputText(mimeType: string, buffer: IOutputItemDto, shortError: boolean = false): string {
let text = `${mimeType}`; // default in case we can't get the text value for some reason.
const charLimit = 100000;
@@ -92,10 +92,10 @@ export function getOutputText(mimeType: string, buffer: IOutputItemDto) {
text = text.replace(/\\u001b\[[0-9;]*m/gi, '');
try {
const error = JSON.parse(text) as Error;
if (error.stack) {
text = error.stack;
} else {
if (!error.stack || shortError) {
text = `${error.name}: ${error.message}`;
} else {
text = error.stack;
}
} catch {
// just use raw text
@@ -3,7 +3,6 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { alert } from '../../../../base/browser/ui/aria/aria.js';
import { Event } from '../../../../base/common/event.js';
import { KeyChord, KeyCode, KeyMod } from '../../../../base/common/keyCodes.js';
import { Disposable } from '../../../../base/common/lifecycle.js';
@@ -18,7 +17,6 @@ import { CodeEditorWidget } from '../../../../editor/browser/widget/codeEditor/c
import { PLAINTEXT_LANGUAGE_ID } from '../../../../editor/common/languages/modesRegistry.js';
import { localize2 } from '../../../../nls.js';
import { AccessibleViewRegistry } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js';
import { CONTEXT_ACCESSIBILITY_MODE_ENABLED } from '../../../../platform/accessibility/common/accessibility.js';
import { Action2, MenuId, registerAction2 } from '../../../../platform/actions/common/actions.js';
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
import { ContextKeyExpr, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
@@ -46,9 +44,8 @@ import { INotebookEditorOptions } from '../../notebook/browser/notebookBrowser.j
import { NotebookEditorWidget } from '../../notebook/browser/notebookEditorWidget.js';
import * as icons from '../../notebook/browser/notebookIcons.js';
import { INotebookEditorService } from '../../notebook/browser/services/notebookEditorService.js';
import { getAllOutputsText } from '../../notebook/browser/viewModel/cellOutputTextHelper.js';
import { CellEditType, CellKind, NotebookSetting, NotebookWorkingCopyTypeIdentifier, REPL_EDITOR_ID } from '../../notebook/common/notebookCommon.js';
import { IS_COMPOSITE_NOTEBOOK, MOST_RECENT_REPL_EDITOR, NOTEBOOK_CELL_LIST_FOCUSED } from '../../notebook/common/notebookContextKeys.js';
import { IS_COMPOSITE_NOTEBOOK, MOST_RECENT_REPL_EDITOR, NOTEBOOK_CELL_LIST_FOCUSED, NOTEBOOK_EDITOR_FOCUSED } from '../../notebook/common/notebookContextKeys.js';
import { NotebookEditorInputOptions } from '../../notebook/common/notebookEditorInput.js';
import { INotebookEditorModelResolverService } from '../../notebook/common/notebookEditorModelResolverService.js';
import { INotebookService } from '../../notebook/common/notebookService.js';
@@ -238,6 +235,11 @@ registerAction2(class extends Action2 {
id: MenuId.CommandPalette,
when: MOST_RECENT_REPL_EDITOR,
},
keybinding: [{
primary: KeyChord(KeyMod.Alt | KeyCode.End, KeyMod.Alt | KeyCode.End),
weight: NOTEBOOK_EDITOR_WIDGET_ACTION_WEIGHT,
when: ContextKeyExpr.or(IS_COMPOSITE_NOTEBOOK, NOTEBOOK_CELL_LIST_FOCUSED.negate())
}],
precondition: MOST_RECENT_REPL_EDITOR
});
}
@@ -284,33 +286,34 @@ registerAction2(class extends Action2 {
registerAction2(class extends Action2 {
constructor() {
super({
id: 'repl.readLastExecutionOutput',
title: localize2('repl.readMostRecentExecution', 'Read Most Recent Execution Output'),
id: 'repl.input.focus',
title: localize2('repl.input.focus', 'Focus Input Editor'),
category: 'REPL',
keybinding: [{
primary: KeyChord(KeyMod.Alt | KeyCode.End, KeyMod.Alt | KeyCode.End),
weight: NOTEBOOK_EDITOR_WIDGET_ACTION_WEIGHT
}],
menu: {
id: MenuId.CommandPalette,
when: MOST_RECENT_REPL_EDITOR,
},
precondition: ContextKeyExpr.and(
ContextKeyExpr.or(IS_COMPOSITE_NOTEBOOK || NOTEBOOK_CELL_LIST_FOCUSED.negate()),
MOST_RECENT_REPL_EDITOR,
CONTEXT_ACCESSIBILITY_MODE_ENABLED)
keybinding: [{
when: ContextKeyExpr.and(IS_COMPOSITE_NOTEBOOK, NOTEBOOK_EDITOR_FOCUSED),
weight: NOTEBOOK_EDITOR_WIDGET_ACTION_WEIGHT,
primary: KeyMod.CtrlCmd | KeyCode.DownArrow
}, {
when: ContextKeyExpr.and(MOST_RECENT_REPL_EDITOR),
weight: KeybindingWeight.WorkbenchContrib + 5,
primary: KeyChord(KeyMod.Alt | KeyCode.Home, KeyMod.Alt | KeyCode.Home),
}]
});
}
async run(accessor: ServicesAccessor, context?: UriComponents): Promise<void> {
async run(accessor: ServicesAccessor): Promise<void> {
const editorService = accessor.get(IEditorService);
const editorControl = editorService.activeEditorPane?.getControl();
const contextKeyService = accessor.get(IContextKeyService);
let notebookEditor: NotebookEditorWidget | undefined;
if (editorControl && isReplEditorControl(editorControl)) {
notebookEditor = editorControl.notebookEditor;
} else {
if (editorControl && isReplEditorControl(editorControl) && editorControl.notebookEditor) {
editorService.activeEditorPane?.focus();
}
else {
const uriString = MOST_RECENT_REPL_EDITOR.getValue(contextKeyService);
const uri = uriString ? URI.parse(uriString) : undefined;
@@ -320,22 +323,7 @@ registerAction2(class extends Action2 {
const replEditor = editorService.findEditors(uri)[0];
if (replEditor) {
const editor = await editorService.openEditor({ resource: uri, options: { preserveFocus: true } }, replEditor.groupId);
const editorControl = editor?.getControl();
if (editorControl && isReplEditorControl(editorControl)) {
notebookEditor = editorControl.notebookEditor;
}
}
}
const viewModel = notebookEditor?.getViewModel();
const notebook = notebookEditor?.textModel;
if (viewModel && notebook) {
const cell = viewModel.getMostRecentlyExecutedCell();
if (cell) {
const text = getAllOutputsText(notebook, cell);
alert(text);
await editorService.openEditor({ resource: uri, options: { preserveFocus: false } }, replEditor.groupId);
}
}
}
@@ -59,6 +59,8 @@ import { ReplInputHintContentWidget } from '../../interactive/browser/replInputH
import { ServiceCollection } from '../../../../platform/instantiation/common/serviceCollection.js';
import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js';
import { localize } from '../../../../nls.js';
import { NotebookViewModel } from '../../notebook/browser/viewModel/notebookViewModelImpl.js';
import { IAccessibilityService } from '../../../../platform/accessibility/common/accessibility.js';
const INTERACTIVE_EDITOR_VIEW_STATE_PREFERENCE_KEY = 'InteractiveEditorViewState';
@@ -129,7 +131,8 @@ export class ReplEditor extends EditorPane implements IEditorPaneWithScrolling {
@IEditorGroupsService editorGroupService: IEditorGroupsService,
@ITextResourceConfigurationService textResourceConfigurationService: ITextResourceConfigurationService,
@INotebookExecutionStateService notebookExecutionStateService: INotebookExecutionStateService,
@IExtensionService extensionService: IExtensionService
@IExtensionService extensionService: IExtensionService,
@IAccessibilityService private readonly _accessibilityService: IAccessibilityService
) {
super(
REPL_EDITOR_ID,
@@ -540,13 +543,31 @@ export class ReplEditor extends EditorPane implements IEditorPaneWithScrolling {
if (addedCells.length) {
const viewModel = notebookWidget.viewModel;
if (viewModel) {
this._notebookWidgetService.updateReplContextKey(viewModel.notebookDocument.uri.toString());
this.handleAppend(notebookWidget, viewModel);
break;
}
}
}
}
private handleAppend(notebookWidget: NotebookEditorWidget, viewModel: NotebookViewModel) {
this._notebookWidgetService.updateReplContextKey(viewModel.notebookDocument.uri.toString());
const navigateToCell = this._configurationService.getValue('accessibility.replEditor.autoFocusReplExecution');
if (this._accessibilityService.isScreenReaderOptimized()) {
if (navigateToCell === 'lastExecution') {
setTimeout(() => {
const lastCellIndex = viewModel.length - 1;
if (lastCellIndex >= 0) {
const cell = viewModel.viewCells[lastCellIndex];
notebookWidget.focusNotebookCell(cell, 'container');
}
}, 0);
} else if (navigateToCell === 'input') {
this._codeEditorWidget.focus();
}
}
}
override setOptions(options: INotebookEditorOptions | undefined): void {
this._notebookWidget.value?.setOptions(options);
super.setOptions(options);
@@ -34,13 +34,12 @@ function getAccessibilityHelpText(): string {
return [
localize('replEditor.overview', 'You are in a REPL Editor which contains in input box to evaluate expressions and a list of previously executed expressions and their output.'),
localize('replEditor.execute', 'The Execute command{0} will evaluate the expression in the input box.', '<keybinding:repl.execute>'),
localize('replEditor.readLastExecution', 'The Read Last Execution Output command{0} will read the output of the last executed item.', '<keybinding:repl.readLastExecutionOutput>'),
localize('replEditor.configReadExecution', 'The setting `accessibility.replEditor.readLastExecutionOutput` controls if output will be automatically read when execution completes.'),
localize('replEditor.focusHistory', 'The Focus History command{0} will move focus to the list of previously executed items.', '<keybinding:interactive.history.focus>'),
localize('replEditor.autoFocusRepl', 'The setting `accessibility.replEditor.autoFocusReplExecution` controls if focus will automatically move to the REPL after executing code.'),
localize('replEditor.focusLastItemAdded', 'The Focus Last executed command{0} will move focus to the last executed item in the REPL history.', '<keybinding:repl.focusLastItemExecuted>'),
localize('replEditor.accessibilityView', 'Run the Open Accessbility View command{0} while navigating the history for an accessible view of the item\'s output.', '<keybinding:editor.action.accessibleView>'),
localize('replEditor.focusLastItemAdded', 'The Focus Last executed command{0} will move focus to the last executed item without needing to first focus on the editor.', '<keybinding:repl.focusLastItemExecuted>'),
localize('replEditor.focusReplInput', 'The Focus Input Editor command{0} will move focus to the REPL input box.', '<keybinding:interactive.input.focus>'),
localize('replEditor.cellNavigation', 'The up and down arrows will also move focus between previously executed items.'),
localize('replEditor.cellNavigation', 'The up and down arrows will also move focus between previously executed items while focused on the REPL history.'),
localize('replEditor.focusReplInput', 'The Focus Input Editor command{0} will move focus to the REPL input box.', '<keybinding:repl.input.focus>'),
localize('replEditor.focusInOutput', 'The Focus Output command{0} will set focus on the output when focused on a previously executed item.', '<keybinding:notebook.cell.focusInOutput>'),
].join('\n');
}
@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import * as dom from '../../../../base/browser/dom.js';
import * as cssJs from '../../../../base/browser/cssValue.js';
import * as cssValue from '../../../../base/browser/cssValue.js';
import { DeferredPromise, timeout } from '../../../../base/common/async.js';
import { debounce, memoize } from '../../../../base/common/decorators.js';
import { DynamicListEventMultiplexer, Emitter, Event, IDynamicListEventMultiplexer } from '../../../../base/common/event.js';
@@ -1258,8 +1258,8 @@ class TerminalEditorStyle extends Themable {
const iconClasses = getUriClasses(instance, colorTheme.type);
if (uri instanceof URI && iconClasses && iconClasses.length > 1) {
css += (
`.monaco-workbench .terminal-tab.${iconClasses[0]}::before` +
`{content: ''; background-image: ${cssJs.asCSSUrl(uri)};}`
cssValue.inline`.monaco-workbench .terminal-tab.${cssValue.className(iconClasses[0])}::before
{content: ''; background-image: ${cssValue.asCSSUrl(uri)};}`
);
}
if (ThemeIcon.isThemeIcon(icon)) {
@@ -1268,10 +1268,8 @@ class TerminalEditorStyle extends Themable {
if (iconContribution) {
const def = productIconTheme.getIcon(iconContribution);
if (def) {
css += (
`.monaco-workbench .terminal-tab.codicon-${icon.id}::before` +
`{content: '${def.fontCharacter}' !important; font-family: ${cssJs.asCSSPropertyValue(def.font?.id ?? 'codicon')} !important;}`
);
css += cssValue.inline`.monaco-workbench .terminal-tab.codicon-${cssValue.className(icon.id)}::before
{content: ${cssValue.stringValue(def.fontCharacter)} !important; font-family: ${cssValue.stringValue(def.font?.id ?? 'codicon')} !important;}`;
}
}
}
@@ -1280,7 +1278,7 @@ class TerminalEditorStyle extends Themable {
// Add colors
const iconForegroundColor = colorTheme.getColor(iconForeground);
if (iconForegroundColor) {
css += `.monaco-workbench .show-file-icons .file-icon.terminal-tab::before { color: ${iconForegroundColor}; }`;
css += cssValue.inline`.monaco-workbench .show-file-icons .file-icon.terminal-tab::before { color: ${iconForegroundColor}; }`;
}
css += getColorStyleContent(colorTheme, true);
@@ -29,6 +29,7 @@ import { ISimpleSelectedSuggestion, SimpleSuggestWidget } from '../../../../serv
import type { ISimpleSuggestWidgetFontInfo } from '../../../../services/suggest/browser/simpleSuggestWidgetRenderer.js';
import { ITerminalCompletionService } from './terminalCompletionService.js';
import { TerminalShellType } from '../../../../../platform/terminal/common/terminal.js';
import { CancellationToken, CancellationTokenSource } from '../../../../../base/common/cancellation.js';
export interface ISuggestController {
isPasting: boolean;
@@ -67,6 +68,8 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest
private _lastUserData?: string;
static lastAcceptedCompletionTimestamp: number = 0;
private _cancellationTokenSource: CancellationTokenSource | undefined;
isPasting: boolean = false;
private readonly _onBell = this._register(new Emitter<void>());
@@ -113,7 +116,7 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest
}));
}
private async _handleCompletionProviders(terminal?: Terminal): Promise<void> {
private async _handleCompletionProviders(terminal: Terminal | undefined, token: CancellationToken): Promise<void> {
// Nothing to handle if the terminal is not attached
if (!terminal?.element || !this._enableWidget || !this._promptInputModel) {
return;
@@ -129,7 +132,7 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest
}
this._requestedCompletionsIndex = this._promptInputModel.cursorIndex;
const providedCompletions = await this._terminalCompletionService.provideCompletions(this._promptInputModel.value, this._promptInputModel.cursorIndex, this._shellType);
if (!providedCompletions?.length) {
if (!providedCompletions?.length || token.isCancellationRequested) {
return;
}
this._onDidReceiveCompletions.fire();
@@ -183,6 +186,9 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest
const lineContext = new LineContext(normalizedLeadingLineContent, this._cursorIndexDelta);
const model = new SimpleCompletionModel(completions.filter(c => !!c.label).map(c => new SimpleCompletionItem(c)), lineContext);
if (token.isCancellationRequested) {
return;
}
this._showCompletions(model);
}
@@ -202,8 +208,13 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest
if (this.isPasting) {
return;
}
await this._handleCompletionProviders(this._terminal);
if (this._cancellationTokenSource) {
this._cancellationTokenSource.cancel();
this._cancellationTokenSource.dispose();
}
this._cancellationTokenSource = new CancellationTokenSource();
const token = this._cancellationTokenSource.token;
await this._handleCompletionProviders(this._terminal, token);
}
private _sync(promptInputState: IPromptInputModelState): void {
@@ -463,6 +474,8 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest
}
hideSuggestWidget(): void {
this._cancellationTokenSource?.cancel();
this._cancellationTokenSource = undefined;
this._currentPromptInputState = undefined;
this._leadingLineContent = undefined;
this._suggestWidget?.hide();
@@ -865,7 +865,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo
private registerManageSyncAction(): void {
const that = this;
const when = ContextKeyExpr.and(CONTEXT_SYNC_ENABLEMENT, CONTEXT_ACCOUNT_STATE.isEqualTo(AccountStatus.Available), CONTEXT_SYNC_STATE.notEqualsTo(SyncStatus.Uninitialized));
const when = ContextKeyExpr.and(CONTEXT_SYNC_ENABLEMENT, CONTEXT_ACCOUNT_STATE.notEqualsTo(AccountStatus.Unavailable), CONTEXT_SYNC_STATE.notEqualsTo(SyncStatus.Uninitialized));
this._register(registerAction2(class SyncStatusAction extends Action2 {
constructor() {
super({
@@ -133,7 +133,7 @@ export class ApplicationConfiguration extends UserSettings {
uriIdentityService: IUriIdentityService,
logService: ILogService,
) {
super(userDataProfilesService.defaultProfile.settingsResource, { scopes: [ConfigurationScope.APPLICATION] }, uriIdentityService.extUri, fileService, logService);
super(userDataProfilesService.defaultProfile.settingsResource, { scopes: [ConfigurationScope.APPLICATION], skipUnregistered: true }, uriIdentityService.extUri, fileService, logService);
this._register(this.onDidChange(() => this.reloadConfigurationScheduler.schedule()));
this.reloadConfigurationScheduler = this._register(new RunOnceScheduler(() => this.loadConfiguration().then(configurationModel => this._onDidChangeConfiguration.fire(configurationModel)), 50));
}
@@ -1737,6 +1737,12 @@ suite('WorkspaceConfigurationService - Profiles', () => {
assert.strictEqual(testObject.getValue('configurationService.profiles.applicationSetting3'), 'defaultProfile');
}));
test('non registering setting should not be read from default profile', () => runWithFakedTimers<void>({ useFakeTimers: true }, async () => {
await fileService.writeFile(instantiationService.get(IUserDataProfilesService).defaultProfile.settingsResource, VSBuffer.fromString('{ "configurationService.profiles.nonregistered": "defaultProfile" }'));
await testObject.reloadConfiguration();
assert.strictEqual(testObject.getValue('configurationService.profiles.nonregistered'), undefined);
}));
test('initialize with custom all profiles settings', () => runWithFakedTimers<void>({ useFakeTimers: true }, async () => {
await testObject.updateValue(APPLY_ALL_PROFILES_SETTING, ['configurationService.profiles.testSetting2'], ConfigurationTarget.USER_LOCAL);
@@ -11,7 +11,7 @@ import { IDisposable, toDisposable, DisposableStore } from '../../../../base/com
import { isThenable } from '../../../../base/common/async.js';
import { LinkedList } from '../../../../base/common/linkedList.js';
import { createStyleSheet, createCSSRule, removeCSSRulesContainingSelector } from '../../../../base/browser/dom.js';
import { asCSSPropertyValue } from '../../../../base/browser/cssValue.js';
import * as cssValue from '../../../../base/browser/cssValue.js';
import { IThemeService } from '../../../../platform/theme/common/themeService.js';
import { ThemeIcon } from '../../../../base/common/themables.js';
import { isFalsyOrWhitespace } from '../../../../base/common/strings.js';
@@ -139,7 +139,7 @@ class DecorationRule {
`.${this.iconBadgeClassName}::after`,
`content: '${definition.fontCharacter}';
color: ${icon.color ? getColor(icon.color.id) : getColor(color)};
font-family: ${asCSSPropertyValue(definition.font?.id ?? 'codicon')};
font-family: ${cssValue.stringValue(definition.font?.id ?? 'codicon')};
font-size: 16px;
margin-right: 14px;
font-weight: normal;
@@ -26,7 +26,7 @@ import { IBrowserWorkbenchEnvironmentService } from '../../environment/browser/e
import { IWebExtensionsScannerService, IWorkbenchExtensionEnablementService, IWorkbenchExtensionManagementService } from '../../extensionManagement/common/extensionManagement.js';
import { IWebWorkerExtensionHostDataProvider, IWebWorkerExtensionHostInitData, WebWorkerExtensionHost } from './webWorkerExtensionHost.js';
import { FetchFileSystemProvider } from './webWorkerFileSystemProvider.js';
import { AbstractExtensionService, IExtensionHostFactory, ResolvedExtensions, checkEnabledAndProposedAPI } from '../common/abstractExtensionService.js';
import { AbstractExtensionService, IExtensionHostFactory, LocalExtensions, RemoteExtensions, ResolvedExtensions, ResolverExtensions, checkEnabledAndProposedAPI, isResolverExtension } from '../common/abstractExtensionService.js';
import { ExtensionDescriptionRegistrySnapshot } from '../common/extensionDescriptionRegistry.js';
import { ExtensionHostKind, ExtensionRunningPreference, IExtensionHostKindPicker, extensionHostKindToString, extensionRunningPreferenceToString } from '../common/extensionHostKind.js';
import { IExtensionManifestPropertiesService } from '../common/extensionManifestPropertiesService.js';
@@ -41,6 +41,7 @@ import { IRemoteAgentService } from '../../remote/common/remoteAgentService.js';
import { IRemoteExplorerService } from '../../remote/common/remoteExplorerService.js';
import { IUserDataInitializationService } from '../../userData/browser/userDataInit.js';
import { IUserDataProfileService } from '../../userDataProfile/common/userDataProfile.js';
import { AsyncIterableEmitter, AsyncIterableObject } from '../../../../base/common/async.js';
export class ExtensionService extends AbstractExtensionService implements IExtensionService {
@@ -80,6 +81,7 @@ export class ExtensionService extends AbstractExtensionService implements IExten
logService
);
super(
{ hasLocalProcess: false, allowRemoteExtensionsInLocalWebWorker: true },
extensionsProposedApi,
extensionHostFactory,
new BrowserExtensionHostKindPicker(logService),
@@ -117,32 +119,45 @@ export class ExtensionService extends AbstractExtensionService implements IExten
this._register(this._fileService.registerProvider(Schemas.https, provider));
}
private _scanWebExtensionsPromise: Promise<IExtensionDescription[]> | undefined;
private async _scanWebExtensions(): Promise<IExtensionDescription[]> {
const system: IExtensionDescription[] = [], user: IExtensionDescription[] = [], development: IExtensionDescription[] = [];
try {
await Promise.all([
this._webExtensionsScannerService.scanSystemExtensions().then(extensions => system.push(...extensions.map(e => toExtensionDescription(e)))),
this._webExtensionsScannerService.scanUserExtensions(this._userDataProfileService.currentProfile.extensionsResource, { skipInvalidExtensions: true }).then(extensions => user.push(...extensions.map(e => toExtensionDescription(e)))),
this._webExtensionsScannerService.scanExtensionsUnderDevelopment().then(extensions => development.push(...extensions.map(e => toExtensionDescription(e, true))))
]);
} catch (error) {
this._logService.error(error);
if (!this._scanWebExtensionsPromise) {
this._scanWebExtensionsPromise = (async () => {
const system: IExtensionDescription[] = [], user: IExtensionDescription[] = [], development: IExtensionDescription[] = [];
try {
await Promise.all([
this._webExtensionsScannerService.scanSystemExtensions().then(extensions => system.push(...extensions.map(e => toExtensionDescription(e)))),
this._webExtensionsScannerService.scanUserExtensions(this._userDataProfileService.currentProfile.extensionsResource, { skipInvalidExtensions: true }).then(extensions => user.push(...extensions.map(e => toExtensionDescription(e)))),
this._webExtensionsScannerService.scanExtensionsUnderDevelopment().then(extensions => development.push(...extensions.map(e => toExtensionDescription(e, true))))
]);
} catch (error) {
this._logService.error(error);
}
return dedupExtensions(system, user, [], development, this._logService);
})();
}
return dedupExtensions(system, user, [], development, this._logService);
return this._scanWebExtensionsPromise;
}
protected async _resolveExtensionsDefault() {
private async _resolveExtensionsDefault(emitter: AsyncIterableEmitter<ResolvedExtensions>) {
const [localExtensions, remoteExtensions] = await Promise.all([
this._scanWebExtensions(),
this._remoteExtensionsScannerService.scanExtensions()
]);
return new ResolvedExtensions(localExtensions, remoteExtensions, /*hasLocalProcess*/false, /*allowRemoteExtensionsInLocalWebWorker*/true);
if (remoteExtensions.length) {
emitter.emitOne(new RemoteExtensions(remoteExtensions));
}
emitter.emitOne(new LocalExtensions(localExtensions));
}
protected async _resolveExtensions(): Promise<ResolvedExtensions> {
protected _resolveExtensions(): AsyncIterable<ResolvedExtensions> {
return new AsyncIterableObject(emitter => this._doResolveExtensions(emitter));
}
private async _doResolveExtensions(emitter: AsyncIterableEmitter<ResolvedExtensions>): Promise<void> {
if (!this._browserEnvironmentService.expectsResolverExtension) {
return this._resolveExtensionsDefault();
return this._resolveExtensionsDefault(emitter);
}
const remoteAuthority = this._environmentService.remoteAuthority!;
@@ -152,6 +167,11 @@ export class ExtensionService extends AbstractExtensionService implements IExten
// override the trust state through the resolver result.
await this._workspaceTrustManagementService.workspaceResolved;
const localExtensions = await this._scanWebExtensions();
const resolverExtensions = localExtensions.filter(extension => isResolverExtension(extension));
if (resolverExtensions.length) {
emitter.emitOne(new ResolverExtensions(resolverExtensions));
}
let resolverResult: ResolverResult;
try {
@@ -163,7 +183,7 @@ export class ExtensionService extends AbstractExtensionService implements IExten
this._remoteAuthorityResolverService._setResolvedAuthorityError(remoteAuthority, err);
// Proceed with the local extension host
return this._resolveExtensionsDefault();
return this._resolveExtensionsDefault(emitter);
}
// set the resolved authority
@@ -181,7 +201,7 @@ export class ExtensionService extends AbstractExtensionService implements IExten
connection.onReconnecting(() => this._resolveAuthorityAgain());
}
return this._resolveExtensionsDefault();
return this._resolveExtensionsDefault(emitter);
}
protected async _onExtensionHostExit(code: number): Promise<void> {
@@ -61,6 +61,9 @@ export abstract class AbstractExtensionService extends Disposable implements IEx
public _serviceBrand: undefined;
private readonly _hasLocalProcess: boolean;
private readonly _allowRemoteExtensionsInLocalWebWorker: boolean;
private readonly _onDidRegisterExtensions = this._register(new Emitter<void>());
public readonly onDidRegisterExtensions = this._onDidRegisterExtensions.event;
@@ -95,6 +98,7 @@ export abstract class AbstractExtensionService extends Disposable implements IEx
private _resolveAuthorityAttempt: number = 0;
constructor(
options: { hasLocalProcess: boolean; allowRemoteExtensionsInLocalWebWorker: boolean },
private readonly _extensionsProposedApi: ExtensionsProposedApi,
private readonly _extensionHostFactory: IExtensionHostFactory,
private readonly _extensionHostKindPicker: IExtensionHostKindPicker,
@@ -118,6 +122,9 @@ export abstract class AbstractExtensionService extends Disposable implements IEx
) {
super();
this._hasLocalProcess = options.hasLocalProcess;
this._allowRemoteExtensionsInLocalWebWorker = options.allowRemoteExtensionsInLocalWebWorker;
// help the file service to activate providers by activating extensions by file system event
this._register(this._fileService.onWillActivateFileSystemProvider(e => {
if (e.scheme !== Schemas.vscodeRemote) {
@@ -318,7 +325,7 @@ export abstract class AbstractExtensionService extends Disposable implements IEx
this._extensionsProposedApi.updateEnabledApiProposals(toAdd);
// Update extension points
this._doHandleExtensionPoints((<IExtensionDescription[]>[]).concat(toAdd).concat(toRemove));
this._doHandleExtensionPoints((<IExtensionDescription[]>[]).concat(toAdd).concat(toRemove), false);
// Update the extension host
await this._updateExtensionsOnExtHosts(result.versionId, toAdd, toRemove.map(e => e.identifier));
@@ -453,10 +460,7 @@ export abstract class AbstractExtensionService extends Disposable implements IEx
const lock = await this._registry.acquireLock('_initialize');
try {
const resolvedExtensions = await this._resolveExtensions();
this._processExtensions(lock, resolvedExtensions);
await this._resolveAndProcessExtensions(lock);
// Start extension hosts which are not automatically started
const snapshot = this._registry.getSnapshot();
for (const extHostManager of this._extensionHostManagers) {
@@ -474,10 +478,24 @@ export abstract class AbstractExtensionService extends Disposable implements IEx
await this._handleExtensionTests();
}
private _processExtensions(lock: ExtensionDescriptionRegistryLock, resolvedExtensions: ResolvedExtensions): void {
const { allowRemoteExtensionsInLocalWebWorker, hasLocalProcess } = resolvedExtensions;
const localExtensions = checkEnabledAndProposedAPI(this._logService, this._extensionEnablementService, this._extensionsProposedApi, resolvedExtensions.local, false);
let remoteExtensions = checkEnabledAndProposedAPI(this._logService, this._extensionEnablementService, this._extensionsProposedApi, resolvedExtensions.remote, false);
private async _resolveAndProcessExtensions(lock: ExtensionDescriptionRegistryLock,): Promise<void> {
let resolverExtensions: IExtensionDescription[] = [];
let localExtensions: IExtensionDescription[] = [];
let remoteExtensions: IExtensionDescription[] = [];
for await (const extensions of this._resolveExtensions()) {
if (extensions instanceof ResolverExtensions) {
resolverExtensions = checkEnabledAndProposedAPI(this._logService, this._extensionEnablementService, this._extensionsProposedApi, extensions.extensions, false);
this._registry.deltaExtensions(lock, resolverExtensions, []);
this._doHandleExtensionPoints(resolverExtensions, true);
}
if (extensions instanceof LocalExtensions) {
localExtensions = checkEnabledAndProposedAPI(this._logService, this._extensionEnablementService, this._extensionsProposedApi, extensions.extensions, false);
}
if (extensions instanceof RemoteExtensions) {
remoteExtensions = checkEnabledAndProposedAPI(this._logService, this._extensionEnablementService, this._extensionsProposedApi, extensions.extensions, false);
}
}
// `initializeRunningLocation` will look at the complete picture (e.g. an extension installed on both sides),
// takes care of duplicates and picks a running location for each extension
@@ -486,8 +504,8 @@ export abstract class AbstractExtensionService extends Disposable implements IEx
this._startExtensionHostsIfNecessary(true, []);
// Some remote extensions could run locally in the web worker, so store them
const remoteExtensionsThatNeedToRunLocally = (allowRemoteExtensionsInLocalWebWorker ? this._runningLocations.filterByExtensionHostKind(remoteExtensions, ExtensionHostKind.LocalWebWorker) : []);
const localProcessExtensions = (hasLocalProcess ? this._runningLocations.filterByExtensionHostKind(localExtensions, ExtensionHostKind.LocalProcess) : []);
const remoteExtensionsThatNeedToRunLocally = (this._allowRemoteExtensionsInLocalWebWorker ? this._runningLocations.filterByExtensionHostKind(remoteExtensions, ExtensionHostKind.LocalWebWorker) : []);
const localProcessExtensions = (this._hasLocalProcess ? this._runningLocations.filterByExtensionHostKind(localExtensions, ExtensionHostKind.LocalProcess) : []);
const localWebWorkerExtensions = this._runningLocations.filterByExtensionHostKind(localExtensions, ExtensionHostKind.LocalWebWorker);
remoteExtensions = this._runningLocations.filterByExtensionHostKind(remoteExtensions, ExtensionHostKind.Remote);
@@ -499,8 +517,22 @@ export abstract class AbstractExtensionService extends Disposable implements IEx
}
const allExtensions = remoteExtensions.concat(localProcessExtensions).concat(localWebWorkerExtensions);
let toAdd = allExtensions;
const result = this._registry.deltaExtensions(lock, allExtensions, []);
if (resolverExtensions.length) {
// Add extensions that are not registered as resolvers but are in the final resolved set
toAdd = allExtensions.filter(extension => !resolverExtensions.some(e => ExtensionIdentifier.equals(e.identifier, extension.identifier) && e.extensionLocation.toString() === extension.extensionLocation.toString()));
// Remove extensions that are registered as resolvers but are not in the final resolved set
if (allExtensions.length < toAdd.length + resolverExtensions.length) {
const toRemove = resolverExtensions.filter(registered => !allExtensions.some(e => ExtensionIdentifier.equals(e.identifier, registered.identifier) && e.extensionLocation.toString() === registered.extensionLocation.toString()));
if (toRemove.length) {
this._registry.deltaExtensions(lock, [], toRemove.map(e => e.identifier));
this._doHandleExtensionPoints(toRemove, true);
}
}
}
const result = this._registry.deltaExtensions(lock, toAdd, []);
if (result.removedDueToLooping.length > 0) {
this._notificationService.notify({
severity: Severity.Error,
@@ -508,7 +540,7 @@ export abstract class AbstractExtensionService extends Disposable implements IEx
});
}
this._doHandleExtensionPoints(this._registry.getAllExtensionDescriptions());
this._doHandleExtensionPoints(this._registry.getAllExtensionDescriptions(), false);
}
private async _handleExtensionTests(): Promise<void> {
@@ -1031,7 +1063,7 @@ export abstract class AbstractExtensionService extends Disposable implements IEx
}
}
private _doHandleExtensionPoints(affectedExtensions: IExtensionDescription[]): void {
private _doHandleExtensionPoints(affectedExtensions: IExtensionDescription[], onlyResolverExtensionPoints: boolean): void {
const affectedExtensionPoints: { [extPointName: string]: boolean } = Object.create(null);
for (const extensionDescription of affectedExtensions) {
if (extensionDescription.contributes) {
@@ -1046,15 +1078,15 @@ export abstract class AbstractExtensionService extends Disposable implements IEx
const messageHandler = (msg: IMessage) => this._handleExtensionPointMessage(msg);
const availableExtensions = this._registry.getAllExtensionDescriptions();
const extensionPoints = ExtensionsRegistry.getExtensionPoints();
perf.mark('code/willHandleExtensionPoints');
perf.mark(onlyResolverExtensionPoints ? 'code/willHandleResolverExtensionPoints' : 'code/willHandleExtensionPoints');
for (const extensionPoint of extensionPoints) {
if (affectedExtensionPoints[extensionPoint.name]) {
if (affectedExtensionPoints[extensionPoint.name] && (!onlyResolverExtensionPoints || extensionPoint.canHandleResolver)) {
perf.mark(`code/willHandleExtensionPoint/${extensionPoint.name}`);
AbstractExtensionService._handleExtensionPoint(extensionPoint, availableExtensions, messageHandler);
perf.mark(`code/didHandleExtensionPoint/${extensionPoint.name}`);
}
}
perf.mark('code/didHandleExtensionPoints');
perf.mark(onlyResolverExtensionPoints ? 'code/didHandleResolverExtensionPoints' : 'code/didHandleExtensionPoints');
}
private _getOrCreateExtensionStatus(extensionId: ExtensionIdentifier): ExtensionStatus {
@@ -1192,7 +1224,7 @@ export abstract class AbstractExtensionService extends Disposable implements IEx
//#endregion
protected abstract _resolveExtensions(): Promise<ResolvedExtensions>;
protected abstract _resolveExtensions(): AsyncIterable<ResolvedExtensions>;
protected abstract _onExtensionHostExit(code: number): Promise<void>;
protected abstract _resolveAuthority(remoteAuthority: string): Promise<ResolverResult>;
}
@@ -1280,15 +1312,26 @@ class ExtensionHostManagerData {
}
}
export class ResolvedExtensions {
export class ResolverExtensions {
constructor(
public readonly local: IExtensionDescription[],
public readonly remote: IExtensionDescription[],
public readonly hasLocalProcess: boolean,
public readonly allowRemoteExtensionsInLocalWebWorker: boolean
public readonly extensions: IExtensionDescription[],
) { }
}
export class LocalExtensions {
constructor(
public readonly extensions: IExtensionDescription[],
) { }
}
export class RemoteExtensions {
constructor(
public readonly extensions: IExtensionDescription[],
) { }
}
export type ResolvedExtensions = ResolverExtensions | LocalExtensions | RemoteExtensions;
export interface IExtensionHostFactory {
createExtensionHost(runningLocations: ExtensionRunningLocationTracker, runningLocation: ExtensionRunningLocation, isInitialStart: boolean): IExtensionHost | null;
}
@@ -1300,6 +1343,10 @@ class DeltaExtensionsQueueItem {
) { }
}
export function isResolverExtension(extension: IExtensionDescription): boolean {
return !!extension.activationEvents?.some(activationEvent => activationEvent.startsWith('onResolveRemoteAuthority:'));
}
/**
* @argument extensions The extensions to be checked.
* @argument ignoreWorkspaceTrust Do not take workspace trust into account.
@@ -70,6 +70,7 @@ export interface IExtensionPoint<T> {
readonly name: string;
setHandler(handler: IExtensionPointHandler<T>): IDisposable;
readonly defaultExtensionKind: ExtensionKind[] | undefined;
readonly canHandleResolver?: boolean;
}
export class ExtensionPointUserDelta<T> {
@@ -109,14 +110,16 @@ export class ExtensionPoint<T> implements IExtensionPoint<T> {
public readonly name: string;
public readonly defaultExtensionKind: ExtensionKind[] | undefined;
public readonly canHandleResolver?: boolean;
private _handler: IExtensionPointHandler<T> | null;
private _users: IExtensionPointUser<T>[] | null;
private _delta: ExtensionPointUserDelta<T> | null;
constructor(name: string, defaultExtensionKind: ExtensionKind[] | undefined) {
constructor(name: string, defaultExtensionKind: ExtensionKind[] | undefined, canHandleResolver?: boolean) {
this.name = name;
this.defaultExtensionKind = defaultExtensionKind;
this.canHandleResolver = canHandleResolver;
this._handler = null;
this._users = null;
this._delta = null;
@@ -608,6 +611,7 @@ export interface IExtensionPointDescriptor<T> {
deps?: IExtensionPoint<any>[];
jsonSchema: IJSONSchema;
defaultExtensionKind?: ExtensionKind[];
canHandleResolver?: boolean;
/**
* A function which runs before the extension point has been validated and which
* should collect automatic activation events from the contribution.
@@ -623,7 +627,7 @@ export class ExtensionsRegistryImpl {
if (this._extensionPoints.has(desc.extensionPoint)) {
throw new Error('Duplicate extension point: ' + desc.extensionPoint);
}
const result = new ExtensionPoint<T>(desc.extensionPoint, desc.defaultExtensionKind);
const result = new ExtensionPoint<T>(desc.extensionPoint, desc.defaultExtensionKind, desc.canHandleResolver);
this._extensionPoints.set(desc.extensionPoint, result);
if (desc.activationEventsGenerator) {
ImplicitActivationEvents.register(desc.extensionPoint, desc.activationEventsGenerator);
@@ -40,7 +40,7 @@ import { IWorkspaceTrustManagementService } from '../../../../platform/workspace
import { IWorkbenchEnvironmentService } from '../../environment/common/environmentService.js';
import { EnablementState, IWorkbenchExtensionEnablementService, IWorkbenchExtensionManagementService } from '../../extensionManagement/common/extensionManagement.js';
import { IWebWorkerExtensionHostDataProvider, IWebWorkerExtensionHostInitData, WebWorkerExtensionHost } from '../browser/webWorkerExtensionHost.js';
import { AbstractExtensionService, ExtensionHostCrashTracker, IExtensionHostFactory, ResolvedExtensions, checkEnabledAndProposedAPI, extensionIsEnabled } from '../common/abstractExtensionService.js';
import { AbstractExtensionService, ExtensionHostCrashTracker, IExtensionHostFactory, LocalExtensions, RemoteExtensions, ResolvedExtensions, ResolverExtensions, checkEnabledAndProposedAPI, extensionIsEnabled, isResolverExtension } from '../common/abstractExtensionService.js';
import { ExtensionDescriptionRegistrySnapshot } from '../common/extensionDescriptionRegistry.js';
import { parseExtensionDevOptions } from '../common/extensionDevOptions.js';
import { ExtensionHostKind, ExtensionRunningPreference, IExtensionHostKindPicker, extensionHostKindToString, extensionRunningPreferenceToString } from '../common/extensionHostKind.js';
@@ -58,6 +58,7 @@ import { IHostService } from '../../host/browser/host.js';
import { ILifecycleService, LifecyclePhase } from '../../lifecycle/common/lifecycle.js';
import { IRemoteAgentService } from '../../remote/common/remoteAgentService.js';
import { IRemoteExplorerService } from '../../remote/common/remoteExplorerService.js';
import { AsyncIterableEmitter, AsyncIterableObject } from '../../../../base/common/async.js';
export class NativeExtensionService extends AbstractExtensionService implements IExtensionService {
@@ -103,6 +104,7 @@ export class NativeExtensionService extends AbstractExtensionService implements
logService
);
super(
{ hasLocalProcess: true, allowRemoteExtensionsInLocalWebWorker: false },
extensionsProposedApi,
extensionHostFactory,
new NativeExtensionHostKindPicker(environmentService, configurationService, logService),
@@ -316,7 +318,11 @@ export class NativeExtensionService extends AbstractExtensionService implements
throw new Error(`Cannot get canonical URI because no extension is installed to resolve ${getRemoteAuthorityPrefix(remoteAuthority)}`);
}
protected async _resolveExtensions(): Promise<ResolvedExtensions> {
protected _resolveExtensions(): AsyncIterable<ResolvedExtensions> {
return new AsyncIterableObject(emitter => this._doResolveExtensions(emitter));
}
private async _doResolveExtensions(emitter: AsyncIterableEmitter<ResolvedExtensions>): Promise<void> {
this._extensionScanner.startScanningExtensions();
const remoteAuthority = this._environmentService.remoteAuthority;
@@ -358,6 +364,12 @@ export class NativeExtensionService extends AbstractExtensionService implements
this._logService.info(`Finished waiting on IWorkspaceTrustManagementService.workspaceResolved.`);
}
const localExtensions = await this._scanAllLocalExtensions();
const resolverExtensions = localExtensions.filter(extension => isResolverExtension(extension));
if (resolverExtensions.length) {
emitter.emitOne(new ResolverExtensions(resolverExtensions));
}
let resolverResult: ResolverResult;
try {
resolverResult = await this._resolveAuthorityInitial(remoteAuthority);
@@ -372,7 +384,7 @@ export class NativeExtensionService extends AbstractExtensionService implements
this._remoteAuthorityResolverService._setResolvedAuthorityError(remoteAuthority, err);
// Proceed with the local extension host
return this._startLocalExtensionHost();
return this._startLocalExtensionHost(emitter);
}
// set the resolved authority
@@ -399,7 +411,7 @@ export class NativeExtensionService extends AbstractExtensionService implements
if (!remoteEnv) {
this._notificationService.notify({ severity: Severity.Error, message: nls.localize('getEnvironmentFailure', "Could not fetch remote environment") });
// Proceed with the local extension host
return this._startLocalExtensionHost();
return this._startLocalExtensionHost(emitter);
}
updateProxyConfigurationsScope(remoteEnv.useHostProxy ? ConfigurationScope.APPLICATION : ConfigurationScope.MACHINE);
@@ -409,15 +421,19 @@ export class NativeExtensionService extends AbstractExtensionService implements
}
return this._startLocalExtensionHost(remoteExtensions);
return this._startLocalExtensionHost(emitter, remoteExtensions);
}
private async _startLocalExtensionHost(remoteExtensions: IExtensionDescription[] = []): Promise<ResolvedExtensions> {
private async _startLocalExtensionHost(emitter: AsyncIterableEmitter<ResolvedExtensions>, remoteExtensions: IExtensionDescription[] = []): Promise<void> {
// Ensure that the workspace trust state has been fully initialized so
// that the extension host can start with the correct set of extensions.
await this._workspaceTrustManagementService.workspaceTrustInitialized;
return new ResolvedExtensions(await this._scanAllLocalExtensions(), remoteExtensions, /*hasLocalProcess*/true, /*allowRemoteExtensionsInLocalWebWorker*/false);
if (remoteExtensions.length) {
emitter.emitOne(new RemoteExtensions(remoteExtensions));
}
emitter.emitOne(new LocalExtensions(await this._scanAllLocalExtensions()));
}
protected async _onExtensionHostExit(code: number): Promise<void> {
@@ -163,6 +163,7 @@ suite('ExtensionService', () => {
}
};
super(
{ allowRemoteExtensionsInLocalWebWorker: false, hasLocalProcess: true },
extensionsProposedApi,
extensionHostFactory,
null!,
@@ -209,7 +210,7 @@ suite('ExtensionService', () => {
}
};
}
protected _resolveExtensions(): Promise<ResolvedExtensions> {
protected _resolveExtensions(): AsyncIterable<ResolvedExtensions> {
throw new Error('Method not implemented.');
}
protected _scanSingleExtension(extension: IExtension): Promise<IExtensionDescription | null> {
@@ -75,7 +75,7 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat
private _authenticationProviders: IAuthenticationProvider[] = [];
get authenticationProviders() { return this._authenticationProviders; }
private _accountStatus: AccountStatus = AccountStatus.Unavailable;
private _accountStatus: AccountStatus = AccountStatus.Uninitialized;
get accountStatus(): AccountStatus { return this._accountStatus; }
private readonly _onDidChangeAccountStatus = this._register(new Emitter<AccountStatus>());
readonly onDidChangeAccountStatus = this._onDidChangeAccountStatus.event;
@@ -144,10 +144,9 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat
}
private updateAuthenticationProviders(): boolean {
this.logService.info('Settings Sync: Updating authentication providers. Authentication Providers from store:', this.userDataSyncStoreManagementService.userDataSyncStore?.authenticationProviders || [].map(({ id }) => id));
const oldValue = this._authenticationProviders;
this._authenticationProviders = (this.userDataSyncStoreManagementService.userDataSyncStore?.authenticationProviders || []).filter(({ id }) => this.authenticationService.declaredProviders.some(provider => provider.id === id));
this.logService.info('Settings Sync: Authentication providers updated', this._authenticationProviders.map(({ id }) => id));
this.logService.trace('Settings Sync: Authentication providers updated', this._authenticationProviders.map(({ id }) => id));
return equals(oldValue, this._authenticationProviders, (a, b) => a.id === b.id);
}
@@ -189,6 +188,7 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat
const initPromise = this.update('initialize');
this._register(this.authenticationService.onDidChangeDeclaredProviders(() => {
if (this.updateAuthenticationProviders()) {
// Trigger update only after the initialization is done
initPromise.finally(() => this.update('declared authentication providers changed'));
}
}));
@@ -223,7 +223,7 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat
}
private async update(reason: string): Promise<void> {
this.logService.info(`Settings Sync: Updating due to ${reason}`);
this.logService.trace(`Settings Sync: Updating due to ${reason}`);
this.updateAuthenticationProviders();
await this.updateCurrentAccount();
@@ -237,18 +237,17 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat
}
private async updateCurrentAccount(): Promise<void> {
this.logService.info('Settings Sync: Updating the current account');
this.logService.trace('Settings Sync: Updating the current account');
const currentSessionId = this.currentSessionId;
const currentAuthenticationProviderId = this.currentAuthenticationProviderId;
if (currentSessionId) {
const authenticationProviders = currentAuthenticationProviderId ? this.authenticationProviders.filter(({ id }) => id === currentAuthenticationProviderId) : this.authenticationProviders;
this.logService.info('Settings Sync: Updating the current account using current session', currentSessionId, currentAuthenticationProviderId, authenticationProviders.map(({ id }) => id));
for (const { id, scopes } of authenticationProviders) {
const sessions = (await this.authenticationService.getSessions(id, scopes)) || [];
for (const session of sessions) {
if (session.id === currentSessionId) {
this._current = new UserDataSyncAccount(id, session);
this.logService.info('Settings Sync: Updated the current account', this._current.accountName);
this.logService.trace('Settings Sync: Updated the current account', this._current.accountName);
return;
}
}
@@ -261,7 +260,7 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat
let value: { token: string; authenticationProviderId: string } | undefined = undefined;
if (current) {
try {
this.logService.info('Settings Sync: Updating the token for the account', current.accountName);
this.logService.trace('Settings Sync: Updating the token for the account', current.accountName);
const token = current.token;
this.logService.info('Settings Sync: Token updated for the account', current.accountName);
value = { token, authenticationProviderId: current.authenticationProviderId };
@@ -273,7 +272,7 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat
}
private updateAccountStatus(accountStatus: AccountStatus): void {
this.logService.info(`Settings Sync: Updating the account status to ${accountStatus}`);
this.logService.trace(`Settings Sync: Updating the account status to ${accountStatus}`);
if (this._accountStatus !== accountStatus) {
const previous = this._accountStatus;
this.logService.info(`Settings Sync: Account status changed from ${previous} to ${accountStatus}`);
@@ -83,7 +83,7 @@ export const SYNC_VIEW_ICON = registerIcon('settings-sync-view-icon', Codicon.sy
// Contexts
export const CONTEXT_SYNC_STATE = new RawContextKey<string>('syncStatus', SyncStatus.Uninitialized);
export const CONTEXT_SYNC_ENABLEMENT = new RawContextKey<boolean>('syncEnabled', false);
export const CONTEXT_ACCOUNT_STATE = new RawContextKey<string>('userDataSyncAccountStatus', AccountStatus.Unavailable);
export const CONTEXT_ACCOUNT_STATE = new RawContextKey<string>('userDataSyncAccountStatus', AccountStatus.Uninitialized);
export const CONTEXT_ENABLE_ACTIVITY_VIEWS = new RawContextKey<boolean>(`enableSyncActivityViews`, false);
export const CONTEXT_ENABLE_SYNC_CONFLICTS_VIEW = new RawContextKey<boolean>(`enableSyncConflictsView`, false);
export const CONTEXT_HAS_CONFLICTS = new RawContextKey<boolean>('hasConflicts', false);
-9
View File
@@ -1,9 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// version: 15
declare module 'vscode' {
}
@@ -0,0 +1,21 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
declare module 'vscode' {
// @CrafterKolyan https://github.com/microsoft/vscode/issues/233274
export interface QuickPick<T> {
/**
* Selection range in the input value. Defined as tuple of two number where the
* first is the inclusive start index and the second the exclusive end index. When
* `undefined` the whole pre-filled value will be selected, when empty (start equals end)
* only the cursor will be set, otherwise the defined range will be selected.
*
* This property does not get updated when the user types or makes a selection,
* but it can be updated by the extension.
*/
valueSelection: readonly [number, number] | undefined;
}
}