mirror of
https://github.com/microsoft/vscode.git
synced 2026-05-08 17:19:48 +01:00
Merge branch 'main' into joh/unique-parrotfish
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "code-oss-dev",
|
||||
"version": "1.96.0",
|
||||
"distro": "4c96b4c357f6753fff9527bbaf38745be0aee80b",
|
||||
"distro": "4460d4a3498ebd6eabd7ab5552d3ca2600026f99",
|
||||
"author": {
|
||||
"name": "Microsoft Corporation"
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)'
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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']
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
+49
-20
@@ -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);
|
||||
}));
|
||||
}
|
||||
|
||||
}
|
||||
+53
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user