Merge pull request #251003 from microsoft/joh/systematic-mockingbird

Detect and fix cyclic runtime dependencies
This commit is contained in:
Johannes Rieken
2025-06-11 09:15:45 +02:00
committed by GitHub
101 changed files with 1688 additions and 1684 deletions
+24 -24
View File
@@ -63,7 +63,7 @@ function createTypeScriptBuilder(config, projectFile, cmd) {
const _log = config.logFn;
const host = new LanguageServiceHost(cmd, projectFile, _log);
const outHost = new LanguageServiceHost({ ...cmd, options: { ...cmd.options, sourceRoot: cmd.options.outDir } }, cmd.options.outDir ?? '', _log);
let lastCycleCheckVersion;
const toBeCheckedForCycles = [];
const service = typescript_1.default.createLanguageService(host, typescript_1.default.createDocumentRegistry());
const lastBuildVersion = Object.create(null);
const lastDtsHash = Object.create(null);
@@ -294,6 +294,7 @@ function createTypeScriptBuilder(config, projectFile, cmd) {
const jsValue = value.files.find(candidate => candidate.basename.endsWith('.js'));
if (jsValue) {
outHost.addScriptSnapshot(jsValue.path, new ScriptSnapshot(String(jsValue.contents), new Date()));
toBeCheckedForCycles.push(normalize(jsValue.path));
}
}).catch(e => {
// can't just skip this or make a result up..
@@ -387,24 +388,21 @@ function createTypeScriptBuilder(config, projectFile, cmd) {
workOnNext();
}).then(() => {
// check for cyclic dependencies
const thisCycleCheckVersion = outHost.getProjectVersion();
if (thisCycleCheckVersion === lastCycleCheckVersion) {
return;
}
const oneCycle = outHost.hasCyclicDependency();
lastCycleCheckVersion = thisCycleCheckVersion;
delete oldErrors[projectFile];
if (oneCycle) {
const cycleError = {
category: typescript_1.default.DiagnosticCategory.Error,
code: 1,
file: undefined,
start: undefined,
length: undefined,
messageText: `CYCLIC dependency between ${oneCycle}`
};
onError(cycleError);
newErrors[projectFile] = [cycleError];
const cycles = outHost.getCyclicDependencies(toBeCheckedForCycles);
toBeCheckedForCycles.length = 0;
for (const [filename, error] of cycles) {
const cyclicDepErrors = [];
if (error) {
cyclicDepErrors.push({
category: typescript_1.default.DiagnosticCategory.Error,
code: 1,
file: undefined,
start: undefined,
length: undefined,
messageText: `CYCLIC dependency: ${error}`
});
}
newErrors[filename] = cyclicDepErrors;
}
}).then(() => {
// store the build versions to not rebuilt the next time
@@ -588,15 +586,17 @@ class LanguageServiceHost {
node.incoming.forEach(entry => target.push(entry.data));
}
}
hasCyclicDependency() {
getCyclicDependencies(filenames) {
// Ensure dependencies are up to date
while (this._dependenciesRecomputeList.length) {
this._processFile(this._dependenciesRecomputeList.pop());
}
const cycle = this._dependencies.findCycle();
return cycle
? cycle.join(' -> ')
: undefined;
const cycles = this._dependencies.findCycles(filenames.sort((a, b) => a.localeCompare(b)));
const result = new Map();
for (const [key, value] of cycles) {
result.set(key, value?.join(' -> '));
}
return result;
}
_processFile(filename) {
if (filename.match(/.*\.d\.ts$/)) {
+24 -24
View File
@@ -44,7 +44,7 @@ export function createTypeScriptBuilder(config: IConfiguration, projectFile: str
const host = new LanguageServiceHost(cmd, projectFile, _log);
const outHost = new LanguageServiceHost({ ...cmd, options: { ...cmd.options, sourceRoot: cmd.options.outDir } }, cmd.options.outDir ?? '', _log);
let lastCycleCheckVersion: string;
const toBeCheckedForCycles: string[] = [];
const service = ts.createLanguageService(host, ts.createDocumentRegistry());
const lastBuildVersion: { [path: string]: string } = Object.create(null);
@@ -315,6 +315,7 @@ export function createTypeScriptBuilder(config: IConfiguration, projectFile: str
const jsValue = value.files.find(candidate => candidate.basename.endsWith('.js'));
if (jsValue) {
outHost.addScriptSnapshot(jsValue.path, new ScriptSnapshot(String(jsValue.contents), new Date()));
toBeCheckedForCycles.push(normalize(jsValue.path));
}
}).catch(e => {
@@ -424,25 +425,22 @@ export function createTypeScriptBuilder(config: IConfiguration, projectFile: str
}).then(() => {
// check for cyclic dependencies
const thisCycleCheckVersion = outHost.getProjectVersion();
if (thisCycleCheckVersion === lastCycleCheckVersion) {
return;
}
const oneCycle = outHost.hasCyclicDependency();
lastCycleCheckVersion = thisCycleCheckVersion;
delete oldErrors[projectFile];
const cycles = outHost.getCyclicDependencies(toBeCheckedForCycles);
toBeCheckedForCycles.length = 0;
if (oneCycle) {
const cycleError: ts.Diagnostic = {
category: ts.DiagnosticCategory.Error,
code: 1,
file: undefined,
start: undefined,
length: undefined,
messageText: `CYCLIC dependency between ${oneCycle}`
};
onError(cycleError);
newErrors[projectFile] = [cycleError];
for (const [filename, error] of cycles) {
const cyclicDepErrors: ts.Diagnostic[] = [];
if (error) {
cyclicDepErrors.push({
category: ts.DiagnosticCategory.Error,
code: 1,
file: undefined,
start: undefined,
length: undefined,
messageText: `CYCLIC dependency: ${error}`
});
}
newErrors[filename] = cyclicDepErrors;
}
}).then(() => {
@@ -666,15 +664,17 @@ class LanguageServiceHost implements ts.LanguageServiceHost {
}
}
hasCyclicDependency(): string | undefined {
getCyclicDependencies(filenames: string[]): Map<string, string | undefined> {
// Ensure dependencies are up to date
while (this._dependenciesRecomputeList.length) {
this._processFile(this._dependenciesRecomputeList.pop()!);
}
const cycle = this._dependencies.findCycle();
return cycle
? cycle.join(' -> ')
: undefined;
const cycles = this._dependencies.findCycles(filenames.sort((a, b) => a.localeCompare(b)));
const result = new Map<string, string | undefined>();
for (const [key, value] of cycles) {
result.set(key, value?.join(' -> '));
}
return result;
}
_processFile(filename: string): void {
+26 -31
View File
@@ -55,44 +55,39 @@ var graph;
lookup(data) {
return this._nodes.get(data) ?? null;
}
findCycle() {
let result;
let foundStartNodes = false;
findCycles(allData) {
const result = new Map();
const checked = new Set();
for (const [_start, value] of this._nodes) {
if (Object.values(value.incoming).length > 0) {
for (const data of allData) {
const node = this.lookup(data);
if (!node) {
continue;
}
foundStartNodes = true;
const dfs = (node, visited) => {
if (checked.has(node)) {
return;
}
if (visited.has(node)) {
result = [...visited, node].map(n => n.data);
const idx = result.indexOf(node.data);
result = result.slice(idx);
return;
}
visited.add(node);
for (const child of Object.values(node.outgoing)) {
dfs(child, visited);
if (result) {
break;
}
}
visited.delete(node);
checked.add(node);
};
dfs(value, new Set());
const r = this._findCycle(node, checked, new Set());
result.set(node.data, r);
}
return result;
}
_findCycle(node, checked, seen) {
if (checked.has(node.data)) {
return undefined;
}
let result;
for (const child of node.outgoing.values()) {
if (seen.has(child.data)) {
const seenArr = Array.from(seen);
const idx = seenArr.indexOf(child.data);
seenArr.push(child.data);
return idx > 0 ? seenArr.slice(idx) : seenArr;
}
seen.add(child.data);
result = this._findCycle(child, checked, seen);
seen.delete(child.data);
if (result) {
break;
}
}
if (!foundStartNodes) {
// everything is a cycle
return Array.from(this._nodes.keys());
}
checked.add(node.data);
return result;
}
}
+27 -38
View File
@@ -63,53 +63,42 @@ export namespace graph {
return this._nodes.get(data) ?? null;
}
findCycle(): T[] | undefined {
let result: T[] | undefined;
let foundStartNodes = false;
const checked = new Set<Node<T>>();
for (const [_start, value] of this._nodes) {
if (Object.values(value.incoming).length > 0) {
findCycles(allData: T[]): Map<T, T[] | undefined> {
const result = new Map<T, T[] | undefined>();
const checked = new Set<T>();
for (const data of allData) {
const node = this.lookup(data);
if (!node) {
continue;
}
const r = this._findCycle(node, checked, new Set());
result.set(node.data, r);
}
return result;
}
foundStartNodes = true;
private _findCycle(node: Node<T>, checked: Set<T>, seen: Set<T>): T[] | undefined {
const dfs = (node: Node<T>, visited: Set<Node<T>>) => {
if (checked.has(node.data)) {
return undefined;
}
if (checked.has(node)) {
return;
}
if (visited.has(node)) {
result = [...visited, node].map(n => n.data);
const idx = result.indexOf(node.data);
result = result.slice(idx);
return;
}
visited.add(node);
for (const child of Object.values(node.outgoing)) {
dfs(child, visited);
if (result) {
break;
}
}
visited.delete(node);
checked.add(node);
};
dfs(value, new Set());
let result: T[] | undefined;
for (const child of node.outgoing.values()) {
if (seen.has(child.data)) {
const seenArr = Array.from(seen);
const idx = seenArr.indexOf(child.data);
seenArr.push(child.data);
return idx > 0 ? seenArr.slice(idx) : seenArr;
}
seen.add(child.data);
result = this._findCycle(child, checked, seen);
seen.delete(child.data);
if (result) {
break;
}
}
if (!foundStartNodes) {
// everything is a cycle
return Array.from(this._nodes.keys());
}
checked.add(node.data);
return result;
}
}
+352 -3
View File
@@ -8,7 +8,7 @@ import { BrowserFeatures } from './canIUse.js';
import { IKeyboardEvent, StandardKeyboardEvent } from './keyboardEvent.js';
import { IMouseEvent, StandardMouseEvent } from './mouseEvent.js';
import { AbstractIdleValue, IntervalTimer, TimeoutTimer, _runWhenIdle, IdleDeadline } from '../common/async.js';
import { onUnexpectedError } from '../common/errors.js';
import { BugIndicatingError, onUnexpectedError } from '../common/errors.js';
import * as event from '../common/event.js';
import dompurify from './dompurify/dompurify.js';
import { KeyCode } from '../common/keyCodes.js';
@@ -19,8 +19,7 @@ import { URI } from '../common/uri.js';
import { hash } from '../common/hash.js';
import { CodeWindow, ensureCodeWindow, mainWindow } from './window.js';
import { isPointWithinTriangle } from '../common/numbers.js';
export * from './domImpl/domObservable.js';
export * from './domImpl/n.js';
import { IObservable, derived, derivedOpts, IReader, observableValue } from '../common/observable.js';
export interface IRegisteredCodeWindow {
readonly window: CodeWindow;
@@ -2395,3 +2394,353 @@ export class SafeTriangle {
return false;
}
}
export namespace n {
function nodeNs<TMap extends Record<string, any>>(elementNs: string | undefined = undefined): DomTagCreateFn<TMap> {
return (tag, attributes, children) => {
const className = attributes.class;
delete attributes.class;
const ref = attributes.ref;
delete attributes.ref;
const obsRef = attributes.obsRef;
delete attributes.obsRef;
return new ObserverNodeWithElement(tag as any, ref, obsRef, elementNs, className, attributes, children);
};
}
function node<TMap extends Record<string, any>, TKey extends keyof TMap>(tag: TKey, elementNs: string | undefined = undefined): DomCreateFn<TMap[TKey], TMap[TKey]> {
const f = nodeNs(elementNs) as any;
return (attributes, children) => {
return f(tag, attributes, children);
};
}
export const div: DomCreateFn<HTMLDivElement, HTMLDivElement> = node<HTMLElementTagNameMap, 'div'>('div');
export const elem = nodeNs<HTMLElementTagNameMap>(undefined);
export const svg: DomCreateFn<SVGElementTagNameMap2['svg'], SVGElement> = node<SVGElementTagNameMap2, 'svg'>('svg', 'http://www.w3.org/2000/svg');
export const svgElem = nodeNs<SVGElementTagNameMap2>('http://www.w3.org/2000/svg');
export function ref<T = HTMLOrSVGElement>(): IRefWithVal<T> {
let value: T | undefined = undefined;
const result: IRef<T> = function (val: T) {
value = val;
};
Object.defineProperty(result, 'element', {
get() {
if (!value) {
throw new BugIndicatingError('Make sure the ref is set before accessing the element. Maybe wrong initialization order?');
}
return value;
}
});
return result as any;
}
}
type Value<T> = T | IObservable<T>;
type ValueOrList<T> = Value<T> | ValueOrList<T>[];
type ValueOrList2<T> = ValueOrList<T> | ValueOrList<ValueOrList<T>>;
type HTMLOrSVGElement = HTMLElement | SVGElement;
type SVGElementTagNameMap2 = {
svg: SVGElement & {
width: number;
height: number;
transform: string;
viewBox: string;
fill: string;
};
path: SVGElement & {
d: string;
stroke: string;
fill: string;
};
linearGradient: SVGElement & {
id: string;
x1: string | number;
x2: string | number;
};
stop: SVGElement & {
offset: string;
};
rect: SVGElement & {
x: number;
y: number;
width: number;
height: number;
fill: string;
};
defs: SVGElement;
};
type DomTagCreateFn<TMap extends Record<string, any>> = <TTag extends keyof TMap>(
tag: TTag,
attributes: ElementAttributeKeys<TMap[TTag]> & { class?: ValueOrList<string | false | undefined>; ref?: IRef<TMap[TTag]>; obsRef?: IRef<ObserverNodeWithElement<TMap[TTag]> | null> },
children?: ChildNode
) => ObserverNode<TMap[TTag]>;
type DomCreateFn<TAttributes, TResult extends HTMLOrSVGElement> = (
attributes: ElementAttributeKeys<TAttributes> & { class?: ValueOrList<string | false | undefined>; ref?: IRef<TResult>; obsRef?: IRef<ObserverNodeWithElement<TResult> | null> },
children?: ChildNode
) => ObserverNode<TResult>;
export type ChildNode = ValueOrList2<HTMLOrSVGElement | string | ObserverNode | undefined>;
export type IRef<T> = (value: T) => void;
export interface IRefWithVal<T> extends IRef<T> {
readonly element: T;
}
export abstract class ObserverNode<T extends HTMLOrSVGElement = HTMLOrSVGElement> {
private readonly _deriveds: (IObservable<any>)[] = [];
protected readonly _element: T;
constructor(
tag: string,
ref: IRef<T> | undefined,
obsRef: IRef<ObserverNodeWithElement<T> | null> | undefined,
ns: string | undefined,
className: ValueOrList<string | undefined | false> | undefined,
attributes: ElementAttributeKeys<T>,
children: ChildNode
) {
this._element = (ns ? document.createElementNS(ns, tag) : document.createElement(tag)) as unknown as T;
if (ref) {
ref(this._element);
}
if (obsRef) {
this._deriveds.push(derived((_reader) => {
obsRef(this as unknown as ObserverNodeWithElement<T>);
_reader.store.add({
dispose: () => {
obsRef(null);
}
});
}));
}
if (className) {
if (hasObservable(className)) {
this._deriveds.push(derived(this, reader => {
/** @description set.class */
setClassName(this._element, getClassName(className, reader));
}));
} else {
setClassName(this._element, getClassName(className, undefined));
}
}
for (const [key, value] of Object.entries(attributes)) {
if (key === 'style') {
for (const [cssKey, cssValue] of Object.entries(value)) {
const key = camelCaseToHyphenCase(cssKey);
if (isObservable(cssValue)) {
this._deriveds.push(derivedOpts({ owner: this, debugName: () => `set.style.${key}` }, reader => {
this._element.style.setProperty(key, convertCssValue(cssValue.read(reader)));
}));
} else {
this._element.style.setProperty(key, convertCssValue(cssValue));
}
}
} else if (key === 'tabIndex') {
if (isObservable(value)) {
this._deriveds.push(derived(this, reader => {
/** @description set.tabIndex */
this._element.tabIndex = value.read(reader) as any;
}));
} else {
this._element.tabIndex = value;
}
} else if (key.startsWith('on')) {
(this._element as any)[key] = value;
} else {
if (isObservable(value)) {
this._deriveds.push(derivedOpts({ owner: this, debugName: () => `set.${key}` }, reader => {
setOrRemoveAttribute(this._element, key, value.read(reader));
}));
} else {
setOrRemoveAttribute(this._element, key, value);
}
}
}
if (children) {
function getChildren(reader: IReader | undefined, children: ValueOrList2<HTMLOrSVGElement | string | ObserverNode | undefined>): (HTMLOrSVGElement | string)[] {
if (isObservable(children)) {
return getChildren(reader, children.read(reader));
}
if (Array.isArray(children)) {
return children.flatMap(c => getChildren(reader, c));
}
if (children instanceof ObserverNode) {
if (reader) {
children.readEffect(reader);
}
return [children._element];
}
if (children) {
return [children];
}
return [];
}
const d = derived(this, reader => {
/** @description set.children */
this._element.replaceChildren(...getChildren(reader, children));
});
this._deriveds.push(d);
if (!childrenIsObservable(children)) {
d.get();
}
}
}
readEffect(reader: IReader | undefined): void {
for (const d of this._deriveds) {
d.read(reader);
}
}
keepUpdated(store: DisposableStore): ObserverNodeWithElement<T> {
derived(reader => {
/** update */
this.readEffect(reader);
}).recomputeInitiallyAndOnChange(store);
return this as unknown as ObserverNodeWithElement<T>;
}
/**
* Creates a live element that will keep the element updated as long as the returned object is not disposed.
*/
toDisposableLiveElement() {
const store = new DisposableStore();
this.keepUpdated(store);
return new LiveElement(this._element, store);
}
}
function setClassName(domNode: HTMLOrSVGElement, className: string) {
if (isSVGElement(domNode)) {
domNode.setAttribute('class', className);
} else {
domNode.className = className;
}
}
function resolve<T>(value: ValueOrList<T>, reader: IReader | undefined, cb: (val: T) => void): void {
if (isObservable(value)) {
cb(value.read(reader));
return;
}
if (Array.isArray(value)) {
for (const v of value) {
resolve(v, reader, cb);
}
return;
}
cb(value as any);
}
function getClassName(className: ValueOrList<string | undefined | false> | undefined, reader: IReader | undefined): string {
let result = '';
resolve(className, reader, val => {
if (val) {
if (result.length === 0) {
result = val;
} else {
result += ' ' + val;
}
}
});
return result;
}
function hasObservable(value: ValueOrList<unknown>): boolean {
if (isObservable(value)) {
return true;
}
if (Array.isArray(value)) {
return value.some(v => hasObservable(v));
}
return false;
}
function convertCssValue(value: any): string {
if (typeof value === 'number') {
return value + 'px';
}
return value;
}
function childrenIsObservable(children: ValueOrList2<HTMLOrSVGElement | string | ObserverNode | undefined>): boolean {
if (isObservable(children)) {
return true;
}
if (Array.isArray(children)) {
return children.some(c => childrenIsObservable(c));
}
return false;
}
export class LiveElement<T extends HTMLOrSVGElement = HTMLElement> {
constructor(
public readonly element: T,
private readonly _disposable: IDisposable
) { }
dispose() {
this._disposable.dispose();
}
}
export class ObserverNodeWithElement<T extends HTMLOrSVGElement = HTMLOrSVGElement> extends ObserverNode<T> {
public get element() {
return this._element;
}
private _isHovered: IObservable<boolean> | undefined = undefined;
get isHovered(): IObservable<boolean> {
if (!this._isHovered) {
const hovered = observableValue<boolean>('hovered', false);
this._element.addEventListener('mouseenter', (_e) => hovered.set(true, undefined));
this._element.addEventListener('mouseleave', (_e) => hovered.set(false, undefined));
this._isHovered = hovered;
}
return this._isHovered;
}
private _didMouseMoveDuringHover: IObservable<boolean> | undefined = undefined;
get didMouseMoveDuringHover(): IObservable<boolean> {
if (!this._didMouseMoveDuringHover) {
let _hovering = false;
const hovered = observableValue<boolean>('didMouseMoveDuringHover', false);
this._element.addEventListener('mouseenter', (_e) => {
_hovering = true;
});
this._element.addEventListener('mousemove', (_e) => {
if (_hovering) {
hovered.set(true, undefined);
}
});
this._element.addEventListener('mouseleave', (_e) => {
_hovering = false;
hovered.set(false, undefined);
});
this._didMouseMoveDuringHover = hovered;
}
return this._didMouseMoveDuringHover;
}
}
function setOrRemoveAttribute(element: HTMLOrSVGElement, key: string, value: unknown) {
if (value === null || value === undefined) {
element.removeAttribute(camelCaseToHyphenCase(key));
} else {
element.setAttribute(camelCaseToHyphenCase(key), String(value));
}
}
function isObservable<T>(obj: unknown): obj is IObservable<T> {
return !!obj && (<IObservable<T>>obj).read !== undefined && (<IObservable<T>>obj).reportChanges !== undefined;
}
type ElementAttributeKeys<T> = Partial<{
[K in keyof T]: T[K] extends Function ? never : T[K] extends object ? ElementAttributeKeys<T[K]> : Value<number | T[K] | undefined | null>;
}>;
@@ -1,17 +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 { DisposableStore, IDisposable } from '../../common/lifecycle.js';
import { autorun, IObservable } from '../../common/observable.js';
import { createStyleSheet2 } from '../domStylesheets.js';
export function createStyleSheetFromObservable(css: IObservable<string>): IDisposable {
const store = new DisposableStore();
const w = store.add(createStyleSheet2());
store.add(autorun(reader => {
w.setStyle(css.read(reader));
}));
return store;
}
-373
View File
@@ -1,373 +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 { BugIndicatingError } from '../../common/errors.js';
import { DisposableStore, IDisposable } from '../../common/lifecycle.js';
import { derived, derivedOpts, IObservable, IReader, observableValue } from '../../common/observable.js';
import { isSVGElement } from '../dom.js';
export namespace n {
function nodeNs<TMap extends Record<string, any>>(elementNs: string | undefined = undefined): DomTagCreateFn<TMap> {
return (tag, attributes, children) => {
const className = attributes.class;
delete attributes.class;
const ref = attributes.ref;
delete attributes.ref;
const obsRef = attributes.obsRef;
delete attributes.obsRef;
return new ObserverNodeWithElement(tag as any, ref, obsRef, elementNs, className, attributes, children);
};
}
function node<TMap extends Record<string, any>, TKey extends keyof TMap>(tag: TKey, elementNs: string | undefined = undefined): DomCreateFn<TMap[TKey], TMap[TKey]> {
const f = nodeNs(elementNs) as any;
return (attributes, children) => {
return f(tag, attributes, children);
};
}
export const div: DomCreateFn<HTMLDivElement, HTMLDivElement> = node<HTMLElementTagNameMap, 'div'>('div');
export const elem = nodeNs<HTMLElementTagNameMap>(undefined);
export const svg: DomCreateFn<SVGElementTagNameMap2['svg'], SVGElement> = node<SVGElementTagNameMap2, 'svg'>('svg', 'http://www.w3.org/2000/svg');
export const svgElem = nodeNs<SVGElementTagNameMap2>('http://www.w3.org/2000/svg');
export function ref<T = Element>(): IRefWithVal<T> {
let value: T | undefined = undefined;
const result: IRef<T> = function (val: T) {
value = val;
};
Object.defineProperty(result, 'element', {
get() {
if (!value) {
throw new BugIndicatingError('Make sure the ref is set before accessing the element. Maybe wrong initialization order?');
}
return value;
}
});
return result as any;
}
}
type Value<T> = T | IObservable<T>;
type ValueOrList<T> = Value<T> | ValueOrList<T>[];
type ValueOrList2<T> = ValueOrList<T> | ValueOrList<ValueOrList<T>>;
type Element = HTMLElement | SVGElement;
type SVGElementTagNameMap2 = {
svg: SVGElement & {
width: number;
height: number;
transform: string;
viewBox: string;
fill: string;
};
path: SVGElement & {
d: string;
stroke: string;
fill: string;
};
linearGradient: SVGElement & {
id: string;
x1: string | number;
x2: string | number;
};
stop: SVGElement & {
offset: string;
};
rect: SVGElement & {
x: number;
y: number;
width: number;
height: number;
fill: string;
};
defs: SVGElement;
};
type DomTagCreateFn<TMap extends Record<string, any>> = <TTag extends keyof TMap>(
tag: TTag,
attributes: ElementAttributeKeys<TMap[TTag]> & { class?: ValueOrList<string | false | undefined>; ref?: IRef<TMap[TTag]>; obsRef?: IRef<ObserverNodeWithElement<TMap[TTag]> | null> },
children?: ChildNode
) => ObserverNode<TMap[TTag]>;
type DomCreateFn<TAttributes, TResult extends Element> = (
attributes: ElementAttributeKeys<TAttributes> & { class?: ValueOrList<string | false | undefined>; ref?: IRef<TResult>; obsRef?: IRef<ObserverNodeWithElement<TResult> | null> },
children?: ChildNode
) => ObserverNode<TResult>;
export type ChildNode = ValueOrList2<Element | string | ObserverNode | undefined>;
export type IRef<T> = (value: T) => void;
export interface IRefWithVal<T> extends IRef<T> {
readonly element: T;
}
export abstract class ObserverNode<T extends Element = Element> {
private readonly _deriveds: (IObservable<any>)[] = [];
protected readonly _element: T;
constructor(
tag: string,
ref: IRef<T> | undefined,
obsRef: IRef<ObserverNodeWithElement<T> | null> | undefined,
ns: string | undefined,
className: ValueOrList<string | undefined | false> | undefined,
attributes: ElementAttributeKeys<T>,
children: ChildNode
) {
this._element = (ns ? document.createElementNS(ns, tag) : document.createElement(tag)) as unknown as T;
if (ref) {
ref(this._element);
}
if (obsRef) {
this._deriveds.push(derived((_reader) => {
obsRef(this as unknown as ObserverNodeWithElement<T>);
_reader.store.add({
dispose: () => {
obsRef(null);
}
});
}));
}
if (className) {
if (hasObservable(className)) {
this._deriveds.push(derived(this, reader => {
/** @description set.class */
setClassName(this._element, getClassName(className, reader));
}));
} else {
setClassName(this._element, getClassName(className, undefined));
}
}
for (const [key, value] of Object.entries(attributes)) {
if (key === 'style') {
for (const [cssKey, cssValue] of Object.entries(value)) {
const key = camelCaseToHyphenCase(cssKey);
if (isObservable(cssValue)) {
this._deriveds.push(derivedOpts({ owner: this, debugName: () => `set.style.${key}` }, reader => {
this._element.style.setProperty(key, convertCssValue(cssValue.read(reader)));
}));
} else {
this._element.style.setProperty(key, convertCssValue(cssValue));
}
}
} else if (key === 'tabIndex') {
if (isObservable(value)) {
this._deriveds.push(derived(this, reader => {
/** @description set.tabIndex */
this._element.tabIndex = value.read(reader) as any;
}));
} else {
this._element.tabIndex = value;
}
} else if (key.startsWith('on')) {
(this._element as any)[key] = value;
} else {
if (isObservable(value)) {
this._deriveds.push(derivedOpts({ owner: this, debugName: () => `set.${key}` }, reader => {
setOrRemoveAttribute(this._element, key, value.read(reader));
}));
} else {
setOrRemoveAttribute(this._element, key, value);
}
}
}
if (children) {
function getChildren(reader: IReader | undefined, children: ValueOrList2<Element | string | ObserverNode | undefined>): (Element | string)[] {
if (isObservable(children)) {
return getChildren(reader, children.read(reader));
}
if (Array.isArray(children)) {
return children.flatMap(c => getChildren(reader, c));
}
if (children instanceof ObserverNode) {
if (reader) {
children.readEffect(reader);
}
return [children._element];
}
if (children) {
return [children];
}
return [];
}
const d = derived(this, reader => {
/** @description set.children */
this._element.replaceChildren(...getChildren(reader, children));
});
this._deriveds.push(d);
if (!childrenIsObservable(children)) {
d.get();
}
}
}
readEffect(reader: IReader | undefined): void {
for (const d of this._deriveds) {
d.read(reader);
}
}
keepUpdated(store: DisposableStore): ObserverNodeWithElement<T> {
derived(reader => {
/** update */
this.readEffect(reader);
}).recomputeInitiallyAndOnChange(store);
return this as unknown as ObserverNodeWithElement<T>;
}
/**
* Creates a live element that will keep the element updated as long as the returned object is not disposed.
*/
toDisposableLiveElement() {
const store = new DisposableStore();
this.keepUpdated(store);
return new LiveElement(this._element, store);
}
}
function setClassName(domNode: Element, className: string) {
if (isSVGElement(domNode)) {
domNode.setAttribute('class', className);
} else {
domNode.className = className;
}
}
function resolve<T>(value: ValueOrList<T>, reader: IReader | undefined, cb: (val: T) => void): void {
if (isObservable(value)) {
cb(value.read(reader));
return;
}
if (Array.isArray(value)) {
for (const v of value) {
resolve(v, reader, cb);
}
return;
}
cb(value as any);
}
function getClassName(className: ValueOrList<string | undefined | false> | undefined, reader: IReader | undefined): string {
let result = '';
resolve(className, reader, val => {
if (val) {
if (result.length === 0) {
result = val;
} else {
result += ' ' + val;
}
}
});
return result;
}
function hasObservable(value: ValueOrList<unknown>): boolean {
if (isObservable(value)) {
return true;
}
if (Array.isArray(value)) {
return value.some(v => hasObservable(v));
}
return false;
}
function convertCssValue(value: any): string {
if (typeof value === 'number') {
return value + 'px';
}
return value;
}
function childrenIsObservable(children: ValueOrList2<Element | string | ObserverNode | undefined>): boolean {
if (isObservable(children)) {
return true;
}
if (Array.isArray(children)) {
return children.some(c => childrenIsObservable(c));
}
return false;
}
export class LiveElement<T extends Element = HTMLElement> {
constructor(
public readonly element: T,
private readonly _disposable: IDisposable
) { }
dispose() {
this._disposable.dispose();
}
}
export class ObserverNodeWithElement<T extends Element = Element> extends ObserverNode<T> {
public get element() {
return this._element;
}
private _isHovered: IObservable<boolean> | undefined = undefined;
get isHovered(): IObservable<boolean> {
if (!this._isHovered) {
const hovered = observableValue<boolean>('hovered', false);
this._element.addEventListener('mouseenter', (_e) => hovered.set(true, undefined));
this._element.addEventListener('mouseleave', (_e) => hovered.set(false, undefined));
this._isHovered = hovered;
}
return this._isHovered;
}
private _didMouseMoveDuringHover: IObservable<boolean> | undefined = undefined;
get didMouseMoveDuringHover(): IObservable<boolean> {
if (!this._didMouseMoveDuringHover) {
let _hovering = false;
const hovered = observableValue<boolean>('didMouseMoveDuringHover', false);
this._element.addEventListener('mouseenter', (_e) => {
_hovering = true;
});
this._element.addEventListener('mousemove', (_e) => {
if (_hovering) {
hovered.set(true, undefined);
}
});
this._element.addEventListener('mouseleave', (_e) => {
_hovering = false;
hovered.set(false, undefined);
});
this._didMouseMoveDuringHover = hovered;
}
return this._didMouseMoveDuringHover;
}
}
function setOrRemoveAttribute(element: Element, key: string, value: unknown) {
if (value === null || value === undefined) {
element.removeAttribute(camelCaseToHyphenCase(key));
} else {
element.setAttribute(camelCaseToHyphenCase(key), String(value));
}
}
function camelCaseToHyphenCase(str: string) {
return str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
}
function isObservable<T>(obj: unknown): obj is IObservable<T> {
return !!obj && (<IObservable<T>>obj).read !== undefined && (<IObservable<T>>obj).reportChanges !== undefined;
}
type ElementAttributeKeys<T> = Partial<{
[K in keyof T]: T[K] extends Function ? never : T[K] extends object ? ElementAttributeKeys<T[K]> : Value<number | T[K] | undefined | null>;
}>;
+10
View File
@@ -4,6 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { DisposableStore, toDisposable, IDisposable } from '../common/lifecycle.js';
import { autorun, IObservable } from '../common/observable.js';
import { getWindows, sharedMutationObserver } from './dom.js';
import { mainWindow } from './window.js';
@@ -166,3 +167,12 @@ export function removeCSSRulesContainingSelector(ruleName: string, style = getSh
function isCSSStyleRule(rule: CSSRule): rule is CSSStyleRule {
return typeof (rule as CSSStyleRule).selectorText === 'string';
}
export function createStyleSheetFromObservable(css: IObservable<string>): IDisposable {
const store = new DisposableStore();
const w = store.add(createStyleSheet2());
store.add(autorun(reader => {
w.setStyle(css.read(reader));
}));
return store;
}
+3
View File
@@ -84,6 +84,9 @@ export namespace Schemas {
/** Scheme used for the chat input editor. */
export const vscodeChatSesssion = 'vscode-chat-editor';
/** Scheme used for the chat input part */
export const vscodeChatInput = 'chatSessionInput';
/**
* Scheme used internally for webviews that aren't linked to a resource (i.e. not custom editors)
*/
+3 -1
View File
@@ -3,7 +3,9 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { observableValueOpts, IObservable, ITransaction } from '../observable.js';
import { IObservable, ITransaction } from '../observable.js';
import { observableValueOpts } from './observables/observableValueOpts.js';
export class ObservableMap<K, V> implements Map<K, V> {
private readonly _data = new Map<K, V>();
+2 -2
View File
@@ -3,8 +3,8 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { observableValueOpts, IObservable, ITransaction } from '../observable.js';
import { IObservable, ITransaction } from '../observable.js';
import { observableValueOpts } from './observables/observableValueOpts.js';
export class ObservableSet<T> implements Set<T> {
+2 -1
View File
@@ -58,7 +58,7 @@ import { ISignService } from '../../platform/sign/common/sign.js';
import { SignService } from '../../platform/sign/node/signService.js';
import { IStateReadService, IStateService } from '../../platform/state/node/state.js';
import { NullTelemetryService } from '../../platform/telemetry/common/telemetryUtils.js';
import { IThemeMainService, ThemeMainService } from '../../platform/theme/electron-main/themeMainService.js';
import { IThemeMainService } from '../../platform/theme/electron-main/themeMainService.js';
import { IUserDataProfilesMainService, UserDataProfilesMainService } from '../../platform/userDataProfile/electron-main/userDataProfile.js';
import { IPolicyService, NullPolicyService } from '../../platform/policy/common/policy.js';
import { NativePolicyService } from '../../platform/policy/node/nativePolicyService.js';
@@ -72,6 +72,7 @@ import { massageMessageBoxOptions } from '../../platform/dialogs/common/dialogs.
import { SaveStrategy, StateService } from '../../platform/state/node/stateService.js';
import { FileUserDataProvider } from '../../platform/userData/common/fileUserDataProvider.js';
import { addUNCHostToAllowlist, getUNCHost } from '../../base/node/unc.js';
import { ThemeMainService } from '../../platform/theme/electron-main/themeMainServiceImpl.js';
/**
* The main VS Code entry point.
@@ -12,6 +12,7 @@ import { NKeyMap } from '../../../../base/common/map.js';
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
import { IThemeService } from '../../../../platform/theme/common/themeService.js';
import { MetadataConsts } from '../../../common/encodedTokenAttributes.js';
import type { DecorationStyleCache } from '../css/decorationStyleCache.js';
import { GlyphRasterizer } from '../raster/glyphRasterizer.js';
import type { IGlyphRasterizer } from '../raster/raster.js';
import { IdleTaskQueue, type ITaskQueue } from '../taskQueue.js';
@@ -59,6 +60,7 @@ export class TextureAtlas extends Disposable {
/** The maximum texture size supported by the GPU. */
private readonly _maxTextureSize: number,
options: ITextureAtlasOptions | undefined,
private readonly _decorationStyleCache: DecorationStyleCache,
@IThemeService private readonly _themeService: IThemeService,
@IInstantiationService private readonly _instantiationService: IInstantiationService
) {
@@ -88,7 +90,7 @@ export class TextureAtlas extends Disposable {
// IMPORTANT: The first glyph on the first page must be an empty glyph such that zeroed out
// cells end up rendering nothing
// TODO: This currently means the first slab is for 0x0 glyphs and is wasted
const nullRasterizer = new GlyphRasterizer(1, '', 1);
const nullRasterizer = new GlyphRasterizer(1, '', 1, this._decorationStyleCache);
firstPage.getGlyph(nullRasterizer, '', 0, 0);
nullRasterizer.dispose();
}
@@ -8,8 +8,8 @@ import { Disposable } from '../../../../base/common/lifecycle.js';
import { isMacintosh } from '../../../../base/common/platform.js';
import { StringBuilder } from '../../../common/core/stringBuilder.js';
import { FontStyle, TokenMetadata } from '../../../common/encodedTokenAttributes.js';
import type { DecorationStyleCache } from '../css/decorationStyleCache.js';
import { ensureNonNullable } from '../gpuUtils.js';
import { ViewGpuContext } from '../viewGpuContext.js';
import { type IBoundingBox, type IGlyphRasterizer, type IRasterizedGlyph } from './raster.js';
let nextId = 0;
@@ -50,7 +50,8 @@ export class GlyphRasterizer extends Disposable implements IGlyphRasterizer {
constructor(
readonly fontSize: number,
readonly fontFamily: string,
readonly devicePixelRatio: number
readonly devicePixelRatio: number,
private readonly _decorationStyleCache: DecorationStyleCache,
) {
super();
@@ -119,7 +120,7 @@ export class GlyphRasterizer extends Disposable implements IGlyphRasterizer {
const bgId = TokenMetadata.getBackground(tokenMetadata);
const bg = colorMap[bgId];
const decorationStyleSet = ViewGpuContext.decorationStyleCache.getStyleSet(decorationStyleSetId);
const decorationStyleSet = this._decorationStyleCache.getStyleSet(decorationStyleSetId);
// When SPAA is used, the background color must be present to get the right glyph
if (this._antiAliasing === 'subpixel') {
+2 -3
View File
@@ -24,13 +24,12 @@ import { Event } from '../../../base/common/event.js';
import { EditorOption, type IEditorOptions } from '../../common/config/editorOptions.js';
import { InlineDecorationType } from '../../common/viewModel.js';
import { DecorationStyleCache } from './css/decorationStyleCache.js';
import { ViewportRenderStrategy } from './renderStrategy/viewportRenderStrategy.js';
export class ViewGpuContext extends Disposable {
/**
* The hard cap for line columns rendered by the GPU renderer.
*/
readonly maxGpuCols = ViewportRenderStrategy.maxSupportedColumns;
readonly maxGpuCols = 2000;
readonly canvas: FastDomNode<HTMLCanvasElement>;
readonly ctx: GPUCanvasContext;
@@ -111,7 +110,7 @@ export class ViewGpuContext extends Disposable {
}).then(ref => {
ViewGpuContext.deviceSync = ref.object;
if (!ViewGpuContext._atlas) {
ViewGpuContext._atlas = this._instantiationService.createInstance(TextureAtlas, ref.object.limits.maxTextureDimension2D, undefined);
ViewGpuContext._atlas = this._instantiationService.createInstance(TextureAtlas, ref.object.limits.maxTextureDimension2D, undefined, ViewGpuContext.decorationStyleCache);
}
return ref.object;
});
@@ -196,7 +196,7 @@ export class ViewLinesGpu extends ViewPart implements IViewLines {
const fontFamily = this._context.configuration.options.get(EditorOption.fontFamily);
const fontSize = this._context.configuration.options.get(EditorOption.fontSize);
this._glyphRasterizer.value = this._register(new GlyphRasterizer(fontSize, fontFamily, this._viewGpuContext.devicePixelRatio.get()));
this._glyphRasterizer.value = this._register(new GlyphRasterizer(fontSize, fontFamily, this._viewGpuContext.devicePixelRatio.get(), ViewGpuContext.decorationStyleCache));
this._register(runOnChange(this._viewGpuContext.devicePixelRatio, () => {
this._refreshGlyphRasterizer();
}));
@@ -454,7 +454,7 @@ export class ViewLinesGpu extends ViewPart implements IViewLines {
glyphRasterizer.fontSize !== fontSize ||
glyphRasterizer.devicePixelRatio !== devicePixelRatio
) {
this._glyphRasterizer.value = new GlyphRasterizer(fontSize, fontFamily, devicePixelRatio);
this._glyphRasterizer.value = new GlyphRasterizer(fontSize, fontFamily, devicePixelRatio, ViewGpuContext.decorationStyleCache);
}
}
@@ -6,7 +6,7 @@
import { assert } from '../../../../base/common/assert.js';
import { splitLines } from '../../../../base/common/strings.js';
import { Position } from '../position.js';
import { PositionOffsetTransformer } from './positionToOffset.js';
import { PositionOffsetTransformer } from './positionToOffsetImpl.js';
import { Range } from '../range.js';
import { LineRange } from '../ranges/lineRange.js';
import { TextLength } from '../text/textLength.js';
@@ -3,117 +3,17 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { findLastIdxMonotonous } from '../../../../base/common/arraysFind.js';
import { StringEdit, StringReplacement } from '../edits/stringEdit.js';
import { OffsetRange } from '../ranges/offsetRange.js';
import { Position } from '../position.js';
import { Range } from '../range.js';
import { TextReplacement, TextEdit } from '../edits/textEdit.js';
import { TextLength } from '../text/textLength.js';
import { TextEdit, TextReplacement } from '../edits/textEdit.js';
import { _setPositionOffsetTransformerDependencies } from './positionToOffsetImpl.js';
import { TextLength } from './textLength.js';
export abstract class PositionOffsetTransformerBase {
abstract getOffset(position: Position): number;
export { PositionOffsetTransformerBase, PositionOffsetTransformer } from './positionToOffsetImpl.js';
getOffsetRange(range: Range): OffsetRange {
return new OffsetRange(
this.getOffset(range.getStartPosition()),
this.getOffset(range.getEndPosition())
);
}
abstract getPosition(offset: number): Position;
getRange(offsetRange: OffsetRange): Range {
return Range.fromPositions(
this.getPosition(offsetRange.start),
this.getPosition(offsetRange.endExclusive)
);
}
getStringEdit(edit: TextEdit): StringEdit {
const edits = edit.replacements.map(e => this.getStringReplacement(e));
return new StringEdit(edits);
}
getStringReplacement(edit: TextReplacement): StringReplacement {
return new StringReplacement(this.getOffsetRange(edit.range), edit.text);
}
getSingleTextEdit(edit: StringReplacement): TextReplacement {
return new TextReplacement(this.getRange(edit.replaceRange), edit.newText);
}
getTextEdit(edit: StringEdit): TextEdit {
const edits = edit.replacements.map(e => this.getSingleTextEdit(e));
return new TextEdit(edits);
}
}
export class PositionOffsetTransformer extends PositionOffsetTransformerBase {
private readonly lineStartOffsetByLineIdx: number[];
private readonly lineEndOffsetByLineIdx: number[];
constructor(public readonly text: string) {
super();
this.lineStartOffsetByLineIdx = [];
this.lineEndOffsetByLineIdx = [];
this.lineStartOffsetByLineIdx.push(0);
for (let i = 0; i < text.length; i++) {
if (text.charAt(i) === '\n') {
this.lineStartOffsetByLineIdx.push(i + 1);
if (i > 0 && text.charAt(i - 1) === '\r') {
this.lineEndOffsetByLineIdx.push(i - 1);
} else {
this.lineEndOffsetByLineIdx.push(i);
}
}
}
this.lineEndOffsetByLineIdx.push(text.length);
}
override getOffset(position: Position): number {
const valPos = this._validatePosition(position);
return this.lineStartOffsetByLineIdx[valPos.lineNumber - 1] + valPos.column - 1;
}
private _validatePosition(position: Position): Position {
if (position.lineNumber < 1) {
return new Position(1, 1);
}
const lineCount = this.textLength.lineCount + 1;
if (position.lineNumber > lineCount) {
const lineLength = this.getLineLength(lineCount);
return new Position(lineCount, lineLength + 1);
}
if (position.column < 1) {
return new Position(position.lineNumber, 1);
}
const lineLength = this.getLineLength(position.lineNumber);
if (position.column - 1 > lineLength) {
return new Position(position.lineNumber, lineLength + 1);
}
return position;
}
override getPosition(offset: number): Position {
const idx = findLastIdxMonotonous(this.lineStartOffsetByLineIdx, i => i <= offset);
const lineNumber = idx + 1;
const column = offset - this.lineStartOffsetByLineIdx[idx] + 1;
return new Position(lineNumber, column);
}
getTextLength(offsetRange: OffsetRange): TextLength {
return TextLength.ofRange(this.getRange(offsetRange));
}
get textLength(): TextLength {
const lineIdx = this.lineStartOffsetByLineIdx.length - 1;
return new TextLength(lineIdx, this.text.length - this.lineStartOffsetByLineIdx[lineIdx]);
}
getLineLength(lineNumber: number): number {
return this.lineEndOffsetByLineIdx[lineNumber - 1] - this.lineStartOffsetByLineIdx[lineNumber - 1];
}
}
_setPositionOffsetTransformerDependencies({
StringEdit: StringEdit,
StringReplacement: StringReplacement,
TextReplacement: TextReplacement,
TextEdit: TextEdit,
TextLength: TextLength,
});
@@ -0,0 +1,142 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { findLastIdxMonotonous } from '../../../../base/common/arraysFind.js';
import { StringEdit, StringReplacement } from '../edits/stringEdit.js';
import { OffsetRange } from '../ranges/offsetRange.js';
import { Position } from '../position.js';
import { Range } from '../range.js';
import type { TextReplacement, TextEdit } from '../edits/textEdit.js';
import type { TextLength } from '../text/textLength.js';
export abstract class PositionOffsetTransformerBase {
abstract getOffset(position: Position): number;
getOffsetRange(range: Range): OffsetRange {
return new OffsetRange(
this.getOffset(range.getStartPosition()),
this.getOffset(range.getEndPosition())
);
}
abstract getPosition(offset: number): Position;
getRange(offsetRange: OffsetRange): Range {
return Range.fromPositions(
this.getPosition(offsetRange.start),
this.getPosition(offsetRange.endExclusive)
);
}
getStringEdit(edit: TextEdit): StringEdit {
const edits = edit.replacements.map(e => this.getStringReplacement(e));
return new Deps.deps.StringEdit(edits);
}
getStringReplacement(edit: TextReplacement): StringReplacement {
return new Deps.deps.StringReplacement(this.getOffsetRange(edit.range), edit.text);
}
getSingleTextEdit(edit: StringReplacement): TextReplacement {
return new Deps.deps.TextReplacement(this.getRange(edit.replaceRange), edit.newText);
}
getTextEdit(edit: StringEdit): TextEdit {
const edits = edit.replacements.map(e => this.getSingleTextEdit(e));
return new Deps.deps.TextEdit(edits);
}
}
interface IDeps {
StringEdit: typeof StringEdit;
StringReplacement: typeof StringReplacement;
TextReplacement: typeof TextReplacement;
TextEdit: typeof TextEdit;
TextLength: typeof TextLength;
}
class Deps {
static _deps: IDeps | undefined = undefined;
static get deps(): IDeps {
if (!this._deps) {
throw new Error('Dependencies not set. Call _setDependencies first.');
}
return this._deps;
}
}
/** This is to break circular module dependencies. */
export function _setPositionOffsetTransformerDependencies(deps: IDeps): void {
Deps._deps = deps;
}
export class PositionOffsetTransformer extends PositionOffsetTransformerBase {
private readonly lineStartOffsetByLineIdx: number[];
private readonly lineEndOffsetByLineIdx: number[];
constructor(public readonly text: string) {
super();
this.lineStartOffsetByLineIdx = [];
this.lineEndOffsetByLineIdx = [];
this.lineStartOffsetByLineIdx.push(0);
for (let i = 0; i < text.length; i++) {
if (text.charAt(i) === '\n') {
this.lineStartOffsetByLineIdx.push(i + 1);
if (i > 0 && text.charAt(i - 1) === '\r') {
this.lineEndOffsetByLineIdx.push(i - 1);
} else {
this.lineEndOffsetByLineIdx.push(i);
}
}
}
this.lineEndOffsetByLineIdx.push(text.length);
}
override getOffset(position: Position): number {
const valPos = this._validatePosition(position);
return this.lineStartOffsetByLineIdx[valPos.lineNumber - 1] + valPos.column - 1;
}
private _validatePosition(position: Position): Position {
if (position.lineNumber < 1) {
return new Position(1, 1);
}
const lineCount = this.textLength.lineCount + 1;
if (position.lineNumber > lineCount) {
const lineLength = this.getLineLength(lineCount);
return new Position(lineCount, lineLength + 1);
}
if (position.column < 1) {
return new Position(position.lineNumber, 1);
}
const lineLength = this.getLineLength(position.lineNumber);
if (position.column - 1 > lineLength) {
return new Position(position.lineNumber, lineLength + 1);
}
return position;
}
override getPosition(offset: number): Position {
const idx = findLastIdxMonotonous(this.lineStartOffsetByLineIdx, i => i <= offset);
const lineNumber = idx + 1;
const column = offset - this.lineStartOffsetByLineIdx[idx] + 1;
return new Position(lineNumber, column);
}
getTextLength(offsetRange: OffsetRange): TextLength {
return Deps.deps.TextLength.ofRange(this.getRange(offsetRange));
}
get textLength(): TextLength {
const lineIdx = this.lineStartOffsetByLineIdx.length - 1;
return new Deps.deps.TextLength(lineIdx, this.text.length - this.lineStartOffsetByLineIdx[lineIdx]);
}
getLineLength(lineNumber: number): number {
return this.lineEndOffsetByLineIdx[lineNumber - 1] - this.lineStartOffsetByLineIdx[lineNumber - 1];
}
}
+1 -1
View File
@@ -23,7 +23,7 @@ import { IModelContentChange, IModelContentChangedEvent, IModelDecorationsChange
import { IGuidesTextModelPart } from './textModelGuides.js';
import { ITokenizationTextModelPart } from './tokenizationTextModelPart.js';
import { UndoRedoGroup } from '../../platform/undoRedo/common/undoRedo.js';
import { TokenArray } from './tokens/tokenArray.js';
import { TokenArray } from './tokens/lineTokens.js';
import { IEditorModel } from './editorCommon.js';
import { TextModelEditReason } from './textModelEditReason.js';
+1 -1
View File
@@ -46,7 +46,7 @@ import { ITokenizationTextModelPart } from '../tokenizationTextModelPart.js';
import { IInstantiationService } from '../../../platform/instantiation/common/instantiation.js';
import { IColorTheme } from '../../../platform/theme/common/themeService.js';
import { IUndoRedoService, ResourceEditStackSnapshot, UndoRedoGroup } from '../../../platform/undoRedo/common/undoRedo.js';
import { TokenArray } from '../tokens/tokenArray.js';
import { TokenArray } from '../tokens/lineTokens.js';
import { SetWithKey } from '../../../base/common/collections.js';
import { TextModelEditReason } from '../textModelEditReason.js';
@@ -3,7 +3,6 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import type * as TreeSitter from '@vscode/tree-sitter-wasm';
import { Emitter, Event } from '../../../../../base/common/event.js';
import { toDisposable } from '../../../../../base/common/lifecycle.js';
import { StandardTokenType } from '../../../encodedTokenAttributes.js';
@@ -196,17 +195,3 @@ export class TreeSitterSyntaxTokenBackend extends AbstractSyntaxTokenBackend {
return model.hasTokens();
}
}
export function rangesEqual(a: TreeSitter.Range, b: TreeSitter.Range) {
return (a.startPosition.row === b.startPosition.row)
&& (a.startPosition.column === b.startPosition.column)
&& (a.endPosition.row === b.endPosition.row)
&& (a.endPosition.column === b.endPosition.column)
&& (a.startIndex === b.startIndex)
&& (a.endIndex === b.endIndex);
}
export function rangesIntersect(a: TreeSitter.Range, b: TreeSitter.Range) {
return (a.startIndex <= b.startIndex && a.endIndex >= b.startIndex) ||
(b.startIndex <= a.startIndex && b.endIndex >= a.startIndex);
}
@@ -13,7 +13,6 @@ import { TextLength } from '../../../core/text/textLength.js';
import { IModelContentChangedEvent, IModelContentChange } from '../../../textModelEvents.js';
import { TextModel } from '../../textModel.js';
import { gotoParent, getClosestPreviousNodes, nextSiblingOrParentSibling, gotoNthChild } from './cursorUtils.js';
import { rangesIntersect, rangesEqual } from './treeSitterSyntaxTokenBackend.js';
import { Range } from '../../../core/range.js';
export class TreeSitterTree extends Disposable {
@@ -445,3 +444,16 @@ function newTimeOutProgressCallback(): (state: TreeSitter.ParseState) => void {
return false;
};
}
export function rangesEqual(a: TreeSitter.Range, b: TreeSitter.Range) {
return (a.startPosition.row === b.startPosition.row)
&& (a.startPosition.column === b.startPosition.column)
&& (a.endPosition.row === b.endPosition.row)
&& (a.endPosition.column === b.endPosition.column)
&& (a.startIndex === b.startIndex)
&& (a.endIndex === b.endIndex);
}
export function rangesIntersect(a: TreeSitter.Range, b: TreeSitter.Range) {
return (a.startIndex <= b.startIndex && a.endIndex >= b.startIndex) ||
(b.startIndex <= a.startIndex && b.endIndex >= a.startIndex);
}
+105 -2
View File
@@ -4,11 +4,10 @@
*--------------------------------------------------------------------------------------------*/
import { ILanguageIdCodec } from '../languages.js';
import { FontStyle, ColorId, StandardTokenType, MetadataConsts, TokenMetadata, ITokenPresentation } from '../encodedTokenAttributes.js';
import { FontStyle, ColorId, StandardTokenType, MetadataConsts, ITokenPresentation, TokenMetadata } from '../encodedTokenAttributes.js';
import { IPosition } from '../core/position.js';
import { ITextModel } from '../model.js';
import { OffsetRange } from '../core/ranges/offsetRange.js';
import { TokenArray, TokenArrayBuilder } from './tokenArray.js';
import { onUnexpectedError } from '../../../base/common/errors.js';
@@ -422,3 +421,107 @@ export function getStandardTokenTypeAtPosition(model: ITextModel, position: IPos
const tokenType = lineTokens.getStandardTokenType(tokenIndex);
return tokenType;
}
/**
* This class represents a sequence of tokens.
* Conceptually, each token has a length and a metadata number.
* A token array might be used to annotate a string with metadata.
* Use {@link TokenArrayBuilder} to efficiently create a token array.
*
* TODO: Make this class more efficient (e.g. by using a Int32Array).
*/
export class TokenArray {
public static fromLineTokens(lineTokens: LineTokens): TokenArray {
const tokenInfo: TokenInfo[] = [];
for (let i = 0; i < lineTokens.getCount(); i++) {
tokenInfo.push(new TokenInfo(lineTokens.getEndOffset(i) - lineTokens.getStartOffset(i), lineTokens.getMetadata(i)));
}
return TokenArray.create(tokenInfo);
}
public static create(tokenInfo: TokenInfo[]): TokenArray {
return new TokenArray(tokenInfo);
}
private constructor(
private readonly _tokenInfo: TokenInfo[]
) { }
public toLineTokens(lineContent: string, decoder: ILanguageIdCodec): LineTokens {
return LineTokens.createFromTextAndMetadata(this.map((r, t) => ({ text: r.substring(lineContent), metadata: t.metadata })), decoder);
}
public forEach(cb: (range: OffsetRange, tokenInfo: TokenInfo) => void): void {
let lengthSum = 0;
for (const tokenInfo of this._tokenInfo) {
const range = new OffsetRange(lengthSum, lengthSum + tokenInfo.length);
cb(range, tokenInfo);
lengthSum += tokenInfo.length;
}
}
public map<T>(cb: (range: OffsetRange, tokenInfo: TokenInfo) => T): T[] {
const result: T[] = [];
let lengthSum = 0;
for (const tokenInfo of this._tokenInfo) {
const range = new OffsetRange(lengthSum, lengthSum + tokenInfo.length);
result.push(cb(range, tokenInfo));
lengthSum += tokenInfo.length;
}
return result;
}
public slice(range: OffsetRange): TokenArray {
const result: TokenInfo[] = [];
let lengthSum = 0;
for (const tokenInfo of this._tokenInfo) {
const tokenStart = lengthSum;
const tokenEndEx = tokenStart + tokenInfo.length;
if (tokenEndEx > range.start) {
if (tokenStart >= range.endExclusive) {
break;
}
const deltaBefore = Math.max(0, range.start - tokenStart);
const deltaAfter = Math.max(0, tokenEndEx - range.endExclusive);
result.push(new TokenInfo(tokenInfo.length - deltaBefore - deltaAfter, tokenInfo.metadata));
}
lengthSum += tokenInfo.length;
}
return TokenArray.create(result);
}
public append(other: TokenArray): TokenArray {
const result: TokenInfo[] = this._tokenInfo.concat(other._tokenInfo);
return TokenArray.create(result);
}
}
export type ITokenMetadata = number;
export class TokenInfo {
constructor(
public readonly length: number,
public readonly metadata: ITokenMetadata
) { }
}
/**
* TODO: Make this class more efficient (e.g. by using a Int32Array).
*/
export class TokenArrayBuilder {
private readonly _tokens: TokenInfo[] = [];
public add(length: number, metadata: ITokenMetadata): void {
this._tokens.push(new TokenInfo(length, metadata));
}
public build(): TokenArray {
return TokenArray.create(this._tokens);
}
}
-109
View File
@@ -1,109 +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 { OffsetRange } from '../core/ranges/offsetRange.js';
import { ILanguageIdCodec } from '../languages.js';
import { LineTokens } from './lineTokens.js';
/**
* This class represents a sequence of tokens.
* Conceptually, each token has a length and a metadata number.
* A token array might be used to annotate a string with metadata.
* Use {@link TokenArrayBuilder} to efficiently create a token array.
*
* TODO: Make this class more efficient (e.g. by using a Int32Array).
*/
export class TokenArray {
public static fromLineTokens(lineTokens: LineTokens): TokenArray {
const tokenInfo: TokenInfo[] = [];
for (let i = 0; i < lineTokens.getCount(); i++) {
tokenInfo.push(new TokenInfo(lineTokens.getEndOffset(i) - lineTokens.getStartOffset(i), lineTokens.getMetadata(i)));
}
return TokenArray.create(tokenInfo);
}
public static create(tokenInfo: TokenInfo[]): TokenArray {
return new TokenArray(tokenInfo);
}
private constructor(
private readonly _tokenInfo: TokenInfo[],
) { }
public toLineTokens(lineContent: string, decoder: ILanguageIdCodec): LineTokens {
return LineTokens.createFromTextAndMetadata(this.map((r, t) => ({ text: r.substring(lineContent), metadata: t.metadata })), decoder);
}
public forEach(cb: (range: OffsetRange, tokenInfo: TokenInfo) => void): void {
let lengthSum = 0;
for (const tokenInfo of this._tokenInfo) {
const range = new OffsetRange(lengthSum, lengthSum + tokenInfo.length);
cb(range, tokenInfo);
lengthSum += tokenInfo.length;
}
}
public map<T>(cb: (range: OffsetRange, tokenInfo: TokenInfo) => T): T[] {
const result: T[] = [];
let lengthSum = 0;
for (const tokenInfo of this._tokenInfo) {
const range = new OffsetRange(lengthSum, lengthSum + tokenInfo.length);
result.push(cb(range, tokenInfo));
lengthSum += tokenInfo.length;
}
return result;
}
public slice(range: OffsetRange): TokenArray {
const result: TokenInfo[] = [];
let lengthSum = 0;
for (const tokenInfo of this._tokenInfo) {
const tokenStart = lengthSum;
const tokenEndEx = tokenStart + tokenInfo.length;
if (tokenEndEx > range.start) {
if (tokenStart >= range.endExclusive) {
break;
}
const deltaBefore = Math.max(0, range.start - tokenStart);
const deltaAfter = Math.max(0, tokenEndEx - range.endExclusive);
result.push(new TokenInfo(tokenInfo.length - deltaBefore - deltaAfter, tokenInfo.metadata));
}
lengthSum += tokenInfo.length;
}
return TokenArray.create(result);
}
public append(other: TokenArray): TokenArray {
const result: TokenInfo[] = this._tokenInfo.concat(other._tokenInfo);
return TokenArray.create(result);
}
}
export type TokenMetadata = number;
export class TokenInfo {
constructor(
public readonly length: number,
public readonly metadata: TokenMetadata,
) { }
}
/**
* TODO: Make this class more efficient (e.g. by using a Int32Array).
*/
export class TokenArrayBuilder {
private readonly _tokens: TokenInfo[] = [];
public add(length: number, metadata: TokenMetadata): void {
this._tokens.push(new TokenInfo(length, metadata));
}
public build(): TokenArray {
return TokenArray.create(this._tokens);
}
}
@@ -105,7 +105,7 @@ class DebugEditorGpuRendererAction extends EditorAction {
const atlas = ViewGpuContext.atlas;
const fontFamily = configurationService.getValue<string>('editor.fontFamily');
const fontSize = configurationService.getValue<number>('editor.fontSize');
const rasterizer = new GlyphRasterizer(fontSize, fontFamily, getActiveWindow().devicePixelRatio);
const rasterizer = new GlyphRasterizer(fontSize, fontFamily, getActiveWindow().devicePixelRatio, ViewGpuContext.decorationStyleCache);
let chars = await quickInputService.input({
prompt: 'Enter a character to draw (prefix with 0x for code point))'
});
@@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { createStyleSheetFromObservable } from '../../../../../base/browser/dom.js';
import { createStyleSheetFromObservable } from '../../../../../base/browser/domStylesheets.js';
import { Disposable } from '../../../../../base/common/lifecycle.js';
import { derived, mapObservableArrayCached, derivedDisposable, constObservable, derivedObservableWithCache, IObservable, ISettableObservable } from '../../../../../base/common/observable.js';
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
@@ -12,14 +12,13 @@ import { asCssVariable } from '../../../../../../../platform/theme/common/colorU
import { IThemeService } from '../../../../../../../platform/theme/common/themeService.js';
import { ICodeEditor } from '../../../../../../browser/editorBrowser.js';
import { ObservableCodeEditor, observableCodeEditor } from '../../../../../../browser/observableCodeEditor.js';
import { Rect } from '../../../../../../common/core/2d/rect.js';
import { LineSource, renderLines, RenderOptions } from '../../../../../../browser/widget/diffEditor/components/diffEditorViewZones/renderLines.js';
import { EditorOption } from '../../../../../../common/config/editorOptions.js';
import { Rect } from '../../../../../../common/core/2d/rect.js';
import { LineRange } from '../../../../../../common/core/ranges/lineRange.js';
import { InlineCompletionDisplayLocation } from '../../../../../../common/languages.js';
import { ILanguageService } from '../../../../../../common/languages/language.js';
import { LineTokens } from '../../../../../../common/tokens/lineTokens.js';
import { TokenArray } from '../../../../../../common/tokens/tokenArray.js';
import { LineTokens, TokenArray } from '../../../../../../common/tokens/lineTokens.js';
import { IInlineEditsView, InlineEditTabAction } from '../inlineEditsViewInterface.js';
import { getEditorBlendedColor, inlineEditIndicatorPrimaryBackground, inlineEditIndicatorSecondaryBackground, inlineEditIndicatorsuccessfulBackground } from '../theme.js';
import { maxContentWidthInRange, rectToProps } from '../utils/utils.js';
@@ -12,16 +12,15 @@ import { editorBackground } from '../../../../../../../platform/theme/common/col
import { asCssVariable } from '../../../../../../../platform/theme/common/colorUtils.js';
import { ICodeEditor } from '../../../../../../browser/editorBrowser.js';
import { ObservableCodeEditor, observableCodeEditor } from '../../../../../../browser/observableCodeEditor.js';
import { Rect } from '../../../../../../common/core/2d/rect.js';
import { LineSource, renderLines, RenderOptions } from '../../../../../../browser/widget/diffEditor/components/diffEditorViewZones/renderLines.js';
import { EditorOption } from '../../../../../../common/config/editorOptions.js';
import { LineRange } from '../../../../../../common/core/ranges/lineRange.js';
import { OffsetRange } from '../../../../../../common/core/ranges/offsetRange.js';
import { Rect } from '../../../../../../common/core/2d/rect.js';
import { Position } from '../../../../../../common/core/position.js';
import { Range } from '../../../../../../common/core/range.js';
import { LineRange } from '../../../../../../common/core/ranges/lineRange.js';
import { OffsetRange } from '../../../../../../common/core/ranges/offsetRange.js';
import { ILanguageService } from '../../../../../../common/languages/language.js';
import { LineTokens } from '../../../../../../common/tokens/lineTokens.js';
import { TokenArray } from '../../../../../../common/tokens/tokenArray.js';
import { LineTokens, TokenArray } from '../../../../../../common/tokens/lineTokens.js';
import { InlineDecoration, InlineDecorationType } from '../../../../../../common/viewModel.js';
import { GhostText, GhostTextPart } from '../../../model/ghostText.js';
import { GhostTextView } from '../../ghostText/ghostTextView.js';
@@ -14,17 +14,16 @@ import { IThemeService } from '../../../../../../../platform/theme/common/themeS
import { IEditorMouseEvent, IViewZoneChangeAccessor } from '../../../../../../browser/editorBrowser.js';
import { EditorMouseEvent } from '../../../../../../browser/editorDom.js';
import { ObservableCodeEditor } from '../../../../../../browser/observableCodeEditor.js';
import { Point } from '../../../../../../common/core/2d/point.js';
import { Rect } from '../../../../../../common/core/2d/rect.js';
import { LineSource, renderLines, RenderOptions } from '../../../../../../browser/widget/diffEditor/components/diffEditorViewZones/renderLines.js';
import { EditorOption } from '../../../../../../common/config/editorOptions.js';
import { Point } from '../../../../../../common/core/2d/point.js';
import { Rect } from '../../../../../../common/core/2d/rect.js';
import { Range } from '../../../../../../common/core/range.js';
import { LineRange } from '../../../../../../common/core/ranges/lineRange.js';
import { OffsetRange } from '../../../../../../common/core/ranges/offsetRange.js';
import { Range } from '../../../../../../common/core/range.js';
import { ILanguageService } from '../../../../../../common/languages/language.js';
import { IModelDecorationOptions, TrackedRangeStickiness } from '../../../../../../common/model.js';
import { LineTokens } from '../../../../../../common/tokens/lineTokens.js';
import { TokenArray } from '../../../../../../common/tokens/tokenArray.js';
import { LineTokens, TokenArray } from '../../../../../../common/tokens/lineTokens.js';
import { InlineDecoration, InlineDecorationType } from '../../../../../../common/viewModel.js';
import { IInlineEditsView, InlineEditTabAction } from '../inlineEditsViewInterface.js';
import { getEditorBlendedColor, getModifiedBorderColor, getOriginalBorderColor, modifiedChangedLineBackgroundColor, originalBackgroundColor } from '../theme.js';
@@ -11,16 +11,15 @@ import { constObservable, derived, IObservable, observableValue } from '../../..
import { editorBackground, editorHoverForeground } from '../../../../../../../platform/theme/common/colorRegistry.js';
import { asCssVariable } from '../../../../../../../platform/theme/common/colorUtils.js';
import { ObservableCodeEditor } from '../../../../../../browser/observableCodeEditor.js';
import { Point } from '../../../../../../common/core/2d/point.js';
import { Rect } from '../../../../../../common/core/2d/rect.js';
import { LineSource, renderLines, RenderOptions } from '../../../../../../browser/widget/diffEditor/components/diffEditorViewZones/renderLines.js';
import { EditorOption } from '../../../../../../common/config/editorOptions.js';
import { Point } from '../../../../../../common/core/2d/point.js';
import { Rect } from '../../../../../../common/core/2d/rect.js';
import { StringReplacement } from '../../../../../../common/core/edits/stringEdit.js';
import { OffsetRange } from '../../../../../../common/core/ranges/offsetRange.js';
import { TextReplacement } from '../../../../../../common/core/edits/textEdit.js';
import { OffsetRange } from '../../../../../../common/core/ranges/offsetRange.js';
import { ILanguageService } from '../../../../../../common/languages/language.js';
import { LineTokens } from '../../../../../../common/tokens/lineTokens.js';
import { TokenArray } from '../../../../../../common/tokens/tokenArray.js';
import { LineTokens, TokenArray } from '../../../../../../common/tokens/lineTokens.js';
import { IInlineEditsView, InlineEditTabAction } from '../inlineEditsViewInterface.js';
import { getModifiedBorderColor, getOriginalBorderColor, modifiedChangedTextOverlayColor, originalChangedTextOverlayColor } from '../theme.js';
import { getEditorValidOverlayRect, mapOutFalsy, rectToProps } from '../utils/utils.js';
@@ -12,6 +12,7 @@ import { TextureAtlas } from '../../../../browser/gpu/atlas/textureAtlas.js';
import { createCodeEditorServices } from '../../testCodeEditor.js';
import { assertIsValidGlyph } from './testUtil.js';
import { TextureAtlasSlabAllocator } from '../../../../browser/gpu/atlas/textureAtlasSlabAllocator.js';
import { DecorationStyleCache } from '../../../../browser/gpu/css/decorationStyleCache.js';
const blackInt = 0x000000FF;
const nullCharMetadata = 0x0;
@@ -79,7 +80,7 @@ suite('TextureAtlas', () => {
setup(() => {
instantiationService = createCodeEditorServices(store);
atlas = store.add(instantiationService.createInstance(TextureAtlas, 2, undefined));
atlas = store.add(instantiationService.createInstance(TextureAtlas, 2, undefined, new DecorationStyleCache()));
glyphRasterizer = new TestGlyphRasterizer();
glyphRasterizer.nextGlyphDimensions = [1, 1];
glyphRasterizer.nextGlyphColor = [0, 0, 0, 0xFF];
@@ -90,7 +91,7 @@ suite('TextureAtlas', () => {
});
test('get multiple glyphs', () => {
atlas = store.add(instantiationService.createInstance(TextureAtlas, 32, undefined));
atlas = store.add(instantiationService.createInstance(TextureAtlas, 32, undefined, new DecorationStyleCache()));
for (let i = 0; i < 10; i++) {
assertIsValidGlyph(atlas.getGlyph(glyphRasterizer, ...getUniqueGlyphId()), atlas);
}
@@ -119,14 +120,14 @@ suite('TextureAtlas', () => {
glyphRasterizer.nextGlyphDimensions = [2, 2];
atlas = store.add(instantiationService.createInstance(TextureAtlas, 32, {
allocatorType: (canvas, textureIndex) => new TextureAtlasSlabAllocator(canvas, textureIndex, { slabW: 1, slabH: 1 })
}));
}, new DecorationStyleCache()));
assertIsValidGlyph(atlas.getGlyph(glyphRasterizer, ...getUniqueGlyphId()), atlas);
});
test('adding a non-first glyph larger than the standard slab size, causing an overflow to a new page', () => {
atlas = store.add(instantiationService.createInstance(TextureAtlas, 2, {
allocatorType: (canvas, textureIndex) => new TextureAtlasSlabAllocator(canvas, textureIndex, { slabW: 1, slabH: 1 })
}));
}, new DecorationStyleCache()));
assertIsValidGlyph(atlas.getGlyph(glyphRasterizer, ...getUniqueGlyphId()), atlas);
strictEqual(atlas.pages.length, 1);
glyphRasterizer.nextGlyphDimensions = [2, 2];
@@ -3,57 +3,11 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import electron from 'electron';
import { Emitter, Event } from '../../../base/common/event.js';
import { Disposable } from '../../../base/common/lifecycle.js';
import { isLinux, isMacintosh, isWindows } from '../../../base/common/platform.js';
import { IConfigurationService } from '../../configuration/common/configuration.js';
import { Event } from '../../../base/common/event.js';
import { createDecorator } from '../../instantiation/common/instantiation.js';
import { IStateService } from '../../state/node/state.js';
import { IPartsSplash } from '../common/themeService.js';
import { IColorScheme } from '../../window/common/window.js';
import { ThemeTypeSelector } from '../common/theme.js';
import { ISingleFolderWorkspaceIdentifier, IWorkspaceIdentifier } from '../../workspace/common/workspace.js';
import { coalesce } from '../../../base/common/arrays.js';
import { getAllWindowsExcludingOffscreen } from '../../windows/electron-main/windows.js';
import { ILogService } from '../../log/common/log.js';
// These default colors match our default themes
// editor background color ("Dark Modern", etc...)
const DEFAULT_BG_LIGHT = '#FFFFFF';
const DEFAULT_BG_DARK = '#1F1F1F';
const DEFAULT_BG_HC_BLACK = '#000000';
const DEFAULT_BG_HC_LIGHT = '#FFFFFF';
const THEME_STORAGE_KEY = 'theme';
const THEME_BG_STORAGE_KEY = 'themeBackground';
const THEME_WINDOW_SPLASH_KEY = 'windowSplash';
const THEME_WINDOW_SPLASH_OVERRIDE_KEY = 'windowSplashWorkspaceOverride';
const AUXILIARYBAR_DEFAULT_VISIBILITY = 'workbench.secondarySideBar.defaultVisibility';
namespace ThemeSettings {
export const DETECT_COLOR_SCHEME = 'window.autoDetectColorScheme';
export const DETECT_HC = 'window.autoDetectHighContrast';
export const SYSTEM_COLOR_THEME = 'window.systemColorTheme';
}
interface IPartSplashOverrideWorkspaces {
[workspaceId: string]: {
sideBarVisible: boolean;
auxiliaryBarVisible: boolean;
};
}
interface IPartsSplashOverride {
layoutInfo: {
sideBarWidth: number;
auxiliaryBarWidth: number;
workspaces: IPartSplashOverrideWorkspaces;
};
}
export const IThemeMainService = createDecorator<IThemeMainService>('themeMainService');
@@ -70,340 +24,3 @@ export interface IThemeMainService {
getColorScheme(): IColorScheme;
}
export class ThemeMainService extends Disposable implements IThemeMainService {
declare readonly _serviceBrand: undefined;
private static readonly DEFAULT_BAR_WIDTH = 300;
private static readonly WORKSPACE_OVERRIDE_LIMIT = 50;
private readonly _onDidChangeColorScheme = this._register(new Emitter<IColorScheme>());
readonly onDidChangeColorScheme = this._onDidChangeColorScheme.event;
constructor(
@IStateService private stateService: IStateService,
@IConfigurationService private configurationService: IConfigurationService,
@ILogService private logService: ILogService
) {
super();
// System Theme
if (!isLinux) {
this._register(this.configurationService.onDidChangeConfiguration(e => {
if (e.affectsConfiguration(ThemeSettings.SYSTEM_COLOR_THEME) || e.affectsConfiguration(ThemeSettings.DETECT_COLOR_SCHEME)) {
this.updateSystemColorTheme();
}
}));
}
this.updateSystemColorTheme();
// Color Scheme changes
this._register(Event.fromNodeEventEmitter(electron.nativeTheme, 'updated')(() => this._onDidChangeColorScheme.fire(this.getColorScheme())));
}
private updateSystemColorTheme(): void {
if (isLinux || this.configurationService.getValue(ThemeSettings.DETECT_COLOR_SCHEME)) {
electron.nativeTheme.themeSource = 'system'; // only with `system` we can detect the system color scheme
} else {
switch (this.configurationService.getValue<'default' | 'auto' | 'light' | 'dark'>(ThemeSettings.SYSTEM_COLOR_THEME)) {
case 'dark':
electron.nativeTheme.themeSource = 'dark';
break;
case 'light':
electron.nativeTheme.themeSource = 'light';
break;
case 'auto':
switch (this.getPreferredBaseTheme() ?? this.getStoredBaseTheme()) {
case ThemeTypeSelector.VS: electron.nativeTheme.themeSource = 'light'; break;
case ThemeTypeSelector.VS_DARK: electron.nativeTheme.themeSource = 'dark'; break;
default: electron.nativeTheme.themeSource = 'system';
}
break;
default:
electron.nativeTheme.themeSource = 'system';
break;
}
}
}
getColorScheme(): IColorScheme {
// high contrast is reflected by the shouldUseInvertedColorScheme property
if (isWindows) {
if (electron.nativeTheme.shouldUseHighContrastColors) {
// shouldUseInvertedColorScheme is dark, !shouldUseInvertedColorScheme is light
return { dark: electron.nativeTheme.shouldUseInvertedColorScheme, highContrast: true };
}
}
// high contrast is set if one of shouldUseInvertedColorScheme or shouldUseHighContrastColors is set,
// reflecting the 'Invert colours' and `Increase contrast` settings in MacOS
else if (isMacintosh) {
if (electron.nativeTheme.shouldUseInvertedColorScheme || electron.nativeTheme.shouldUseHighContrastColors) {
return { dark: electron.nativeTheme.shouldUseDarkColors, highContrast: true };
}
}
// ubuntu gnome seems to have 3 states, light dark and high contrast
else if (isLinux) {
if (electron.nativeTheme.shouldUseHighContrastColors) {
return { dark: true, highContrast: true };
}
}
return {
dark: electron.nativeTheme.shouldUseDarkColors,
highContrast: false
};
}
getPreferredBaseTheme(): ThemeTypeSelector | undefined {
const colorScheme = this.getColorScheme();
if (this.configurationService.getValue(ThemeSettings.DETECT_HC) && colorScheme.highContrast) {
return colorScheme.dark ? ThemeTypeSelector.HC_BLACK : ThemeTypeSelector.HC_LIGHT;
}
if (this.configurationService.getValue(ThemeSettings.DETECT_COLOR_SCHEME)) {
return colorScheme.dark ? ThemeTypeSelector.VS_DARK : ThemeTypeSelector.VS;
}
return undefined;
}
getBackgroundColor(): string {
const preferred = this.getPreferredBaseTheme();
const stored = this.getStoredBaseTheme();
// If the stored theme has the same base as the preferred, we can return the stored background
if (preferred === undefined || preferred === stored) {
const storedBackground = this.stateService.getItem<string | null>(THEME_BG_STORAGE_KEY, null);
if (storedBackground) {
return storedBackground;
}
}
// Otherwise we return the default background for the preferred base theme. If there's no preferred, use the stored one.
switch (preferred ?? stored) {
case ThemeTypeSelector.VS: return DEFAULT_BG_LIGHT;
case ThemeTypeSelector.HC_BLACK: return DEFAULT_BG_HC_BLACK;
case ThemeTypeSelector.HC_LIGHT: return DEFAULT_BG_HC_LIGHT;
default: return DEFAULT_BG_DARK;
}
}
private getStoredBaseTheme(): ThemeTypeSelector {
const baseTheme = this.stateService.getItem<ThemeTypeSelector>(THEME_STORAGE_KEY, ThemeTypeSelector.VS_DARK).split(' ')[0];
switch (baseTheme) {
case ThemeTypeSelector.VS: return ThemeTypeSelector.VS;
case ThemeTypeSelector.HC_BLACK: return ThemeTypeSelector.HC_BLACK;
case ThemeTypeSelector.HC_LIGHT: return ThemeTypeSelector.HC_LIGHT;
default: return ThemeTypeSelector.VS_DARK;
}
}
saveWindowSplash(windowId: number | undefined, workspace: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | undefined, splash: IPartsSplash): void {
// Update override as needed
const splashOverride = this.updateWindowSplashOverride(workspace, splash);
// Update in storage
this.stateService.setItems(coalesce([
{ key: THEME_STORAGE_KEY, data: splash.baseTheme },
{ key: THEME_BG_STORAGE_KEY, data: splash.colorInfo.background },
{ key: THEME_WINDOW_SPLASH_KEY, data: splash },
splashOverride ? { key: THEME_WINDOW_SPLASH_OVERRIDE_KEY, data: splashOverride } : undefined
]));
// Update in opened windows
if (typeof windowId === 'number') {
this.updateBackgroundColor(windowId, splash);
}
// Update system theme
this.updateSystemColorTheme();
}
private updateWindowSplashOverride(workspace: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | undefined, splash: IPartsSplash): IPartsSplashOverride | undefined {
let splashOverride: IPartsSplashOverride | undefined = undefined;
let changed = false;
if (workspace) {
splashOverride = { ...this.getWindowSplashOverride() }; // make a copy for modifications
changed = this.doUpdateWindowSplashOverride(workspace, splash, splashOverride, 'sideBar');
changed = this.doUpdateWindowSplashOverride(workspace, splash, splashOverride, 'auxiliaryBar') || changed;
}
return changed ? splashOverride : undefined;
}
private doUpdateWindowSplashOverride(workspace: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier, splash: IPartsSplash, splashOverride: IPartsSplashOverride, part: 'sideBar' | 'auxiliaryBar'): boolean {
const currentWidth = part === 'sideBar' ? splash.layoutInfo?.sideBarWidth : splash.layoutInfo?.auxiliarySideBarWidth;
const overrideWidth = part === 'sideBar' ? splashOverride.layoutInfo.sideBarWidth : splashOverride.layoutInfo.auxiliaryBarWidth;
// No layout info: remove override
let changed = false;
if (typeof currentWidth !== 'number') {
if (splashOverride.layoutInfo.workspaces[workspace.id]) {
delete splashOverride.layoutInfo.workspaces[workspace.id];
changed = true;
}
return changed;
}
let workspaceOverride = splashOverride.layoutInfo.workspaces[workspace.id];
if (!workspaceOverride) {
const workspaceEntries = Object.keys(splashOverride.layoutInfo.workspaces);
if (workspaceEntries.length >= ThemeMainService.WORKSPACE_OVERRIDE_LIMIT) {
delete splashOverride.layoutInfo.workspaces[workspaceEntries[0]];
changed = true;
}
workspaceOverride = { sideBarVisible: false, auxiliaryBarVisible: false };
splashOverride.layoutInfo.workspaces[workspace.id] = workspaceOverride;
changed = true;
}
// Part has width: update width & visibility override
if (currentWidth > 0) {
if (overrideWidth !== currentWidth) {
splashOverride.layoutInfo[part === 'sideBar' ? 'sideBarWidth' : 'auxiliaryBarWidth'] = currentWidth;
changed = true;
}
switch (part) {
case 'sideBar':
if (!workspaceOverride.sideBarVisible) {
workspaceOverride.sideBarVisible = true;
changed = true;
}
break;
case 'auxiliaryBar':
if (!workspaceOverride.auxiliaryBarVisible) {
workspaceOverride.auxiliaryBarVisible = true;
changed = true;
}
break;
}
}
// Part is hidden: update visibility override
else {
switch (part) {
case 'sideBar':
if (workspaceOverride.sideBarVisible) {
workspaceOverride.sideBarVisible = false;
changed = true;
}
break;
case 'auxiliaryBar':
if (workspaceOverride.auxiliaryBarVisible) {
workspaceOverride.auxiliaryBarVisible = false;
changed = true;
}
break;
}
}
return changed;
}
private updateBackgroundColor(windowId: number, splash: IPartsSplash): void {
for (const window of getAllWindowsExcludingOffscreen()) {
if (window.id === windowId) {
window.setBackgroundColor(splash.colorInfo.background);
break;
}
}
}
getWindowSplash(workspace: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | undefined): IPartsSplash | undefined {
try {
return this.doGetWindowSplash(workspace);
} catch (error) {
this.logService.error('[theme main service] Failed to get window splash', error);
return undefined;
}
}
private doGetWindowSplash(workspace: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | undefined): IPartsSplash | undefined {
const partSplash = this.stateService.getItem<IPartsSplash>(THEME_WINDOW_SPLASH_KEY);
if (!partSplash?.layoutInfo) {
return partSplash; // return early: overrides currently only apply to layout info
}
const override = this.getWindowSplashOverride();
// Figure out side bar width based on workspace and overrides
let sideBarWidth: number;
if (workspace) {
if (override.layoutInfo.workspaces[workspace.id]?.sideBarVisible === false) {
sideBarWidth = 0;
} else {
sideBarWidth = override.layoutInfo.sideBarWidth || partSplash.layoutInfo.sideBarWidth || ThemeMainService.DEFAULT_BAR_WIDTH;
}
} else {
sideBarWidth = 0;
}
// Figure out auxiliary bar width based on workspace, configuration and overrides
const auxiliarySideBarDefaultVisibility = this.configurationService.getValue(AUXILIARYBAR_DEFAULT_VISIBILITY);
let auxiliarySideBarWidth: number;
if (workspace) {
const auxiliaryBarVisible = override.layoutInfo.workspaces[workspace.id]?.auxiliaryBarVisible;
if (auxiliaryBarVisible === true) {
auxiliarySideBarWidth = override.layoutInfo.auxiliaryBarWidth || partSplash.layoutInfo.auxiliarySideBarWidth || ThemeMainService.DEFAULT_BAR_WIDTH;
} else if (auxiliaryBarVisible === false) {
auxiliarySideBarWidth = 0;
} else {
if (auxiliarySideBarDefaultVisibility === 'visible' || auxiliarySideBarDefaultVisibility === 'visibleInWorkspace') {
auxiliarySideBarWidth = override.layoutInfo.auxiliaryBarWidth || partSplash.layoutInfo.auxiliarySideBarWidth || ThemeMainService.DEFAULT_BAR_WIDTH;
} else {
auxiliarySideBarWidth = 0;
}
}
} else {
auxiliarySideBarWidth = 0; // technically not true if configured 'visible', but we never store splash per empty window, so we decide on a default here
}
return {
...partSplash,
layoutInfo: {
...partSplash.layoutInfo,
sideBarWidth,
auxiliarySideBarWidth
}
};
}
private getWindowSplashOverride(): IPartsSplashOverride {
let override = this.stateService.getItem<IPartsSplashOverride>(THEME_WINDOW_SPLASH_OVERRIDE_KEY);
if (!override?.layoutInfo) {
override = {
layoutInfo: {
sideBarWidth: ThemeMainService.DEFAULT_BAR_WIDTH,
auxiliaryBarWidth: ThemeMainService.DEFAULT_BAR_WIDTH,
workspaces: {}
}
};
}
if (!override.layoutInfo.sideBarWidth) {
override.layoutInfo.sideBarWidth = ThemeMainService.DEFAULT_BAR_WIDTH;
}
if (!override.layoutInfo.auxiliaryBarWidth) {
override.layoutInfo.auxiliaryBarWidth = ThemeMainService.DEFAULT_BAR_WIDTH;
}
if (!override.layoutInfo.workspaces) {
override.layoutInfo.workspaces = {};
}
return override;
}
}
@@ -0,0 +1,393 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import electron from 'electron';
import { Emitter, Event } from '../../../base/common/event.js';
import { Disposable } from '../../../base/common/lifecycle.js';
import { isLinux, isMacintosh, isWindows } from '../../../base/common/platform.js';
import { IConfigurationService } from '../../configuration/common/configuration.js';
import { IStateService } from '../../state/node/state.js';
import { IPartsSplash } from '../common/themeService.js';
import { IColorScheme } from '../../window/common/window.js';
import { ThemeTypeSelector } from '../common/theme.js';
import { ISingleFolderWorkspaceIdentifier, IWorkspaceIdentifier } from '../../workspace/common/workspace.js';
import { coalesce } from '../../../base/common/arrays.js';
import { getAllWindowsExcludingOffscreen } from '../../windows/electron-main/windows.js';
import { ILogService } from '../../log/common/log.js';
import { IThemeMainService } from './themeMainService.js';
// These default colors match our default themes
// editor background color ("Dark Modern", etc...)
const DEFAULT_BG_LIGHT = '#FFFFFF';
const DEFAULT_BG_DARK = '#1F1F1F';
const DEFAULT_BG_HC_BLACK = '#000000';
const DEFAULT_BG_HC_LIGHT = '#FFFFFF';
const THEME_STORAGE_KEY = 'theme';
const THEME_BG_STORAGE_KEY = 'themeBackground';
const THEME_WINDOW_SPLASH_KEY = 'windowSplash';
const THEME_WINDOW_SPLASH_OVERRIDE_KEY = 'windowSplashWorkspaceOverride';
const AUXILIARYBAR_DEFAULT_VISIBILITY = 'workbench.secondarySideBar.defaultVisibility';
namespace ThemeSettings {
export const DETECT_COLOR_SCHEME = 'window.autoDetectColorScheme';
export const DETECT_HC = 'window.autoDetectHighContrast';
export const SYSTEM_COLOR_THEME = 'window.systemColorTheme';
}
interface IPartSplashOverrideWorkspaces {
[workspaceId: string]: {
sideBarVisible: boolean;
auxiliaryBarVisible: boolean;
};
}
interface IPartsSplashOverride {
layoutInfo: {
sideBarWidth: number;
auxiliaryBarWidth: number;
workspaces: IPartSplashOverrideWorkspaces;
};
}
export class ThemeMainService extends Disposable implements IThemeMainService {
declare readonly _serviceBrand: undefined;
private static readonly DEFAULT_BAR_WIDTH = 300;
private static readonly WORKSPACE_OVERRIDE_LIMIT = 50;
private readonly _onDidChangeColorScheme = this._register(new Emitter<IColorScheme>());
readonly onDidChangeColorScheme = this._onDidChangeColorScheme.event;
constructor(
@IStateService private stateService: IStateService,
@IConfigurationService private configurationService: IConfigurationService,
@ILogService private logService: ILogService
) {
super();
// System Theme
if (!isLinux) {
this._register(this.configurationService.onDidChangeConfiguration(e => {
if (e.affectsConfiguration(ThemeSettings.SYSTEM_COLOR_THEME) || e.affectsConfiguration(ThemeSettings.DETECT_COLOR_SCHEME)) {
this.updateSystemColorTheme();
}
}));
}
this.updateSystemColorTheme();
// Color Scheme changes
this._register(Event.fromNodeEventEmitter(electron.nativeTheme, 'updated')(() => this._onDidChangeColorScheme.fire(this.getColorScheme())));
}
private updateSystemColorTheme(): void {
if (isLinux || this.configurationService.getValue(ThemeSettings.DETECT_COLOR_SCHEME)) {
electron.nativeTheme.themeSource = 'system'; // only with `system` we can detect the system color scheme
} else {
switch (this.configurationService.getValue<'default' | 'auto' | 'light' | 'dark'>(ThemeSettings.SYSTEM_COLOR_THEME)) {
case 'dark':
electron.nativeTheme.themeSource = 'dark';
break;
case 'light':
electron.nativeTheme.themeSource = 'light';
break;
case 'auto':
switch (this.getPreferredBaseTheme() ?? this.getStoredBaseTheme()) {
case ThemeTypeSelector.VS: electron.nativeTheme.themeSource = 'light'; break;
case ThemeTypeSelector.VS_DARK: electron.nativeTheme.themeSource = 'dark'; break;
default: electron.nativeTheme.themeSource = 'system';
}
break;
default:
electron.nativeTheme.themeSource = 'system';
break;
}
}
}
getColorScheme(): IColorScheme {
// high contrast is reflected by the shouldUseInvertedColorScheme property
if (isWindows) {
if (electron.nativeTheme.shouldUseHighContrastColors) {
// shouldUseInvertedColorScheme is dark, !shouldUseInvertedColorScheme is light
return { dark: electron.nativeTheme.shouldUseInvertedColorScheme, highContrast: true };
}
}
// high contrast is set if one of shouldUseInvertedColorScheme or shouldUseHighContrastColors is set,
// reflecting the 'Invert colours' and `Increase contrast` settings in MacOS
else if (isMacintosh) {
if (electron.nativeTheme.shouldUseInvertedColorScheme || electron.nativeTheme.shouldUseHighContrastColors) {
return { dark: electron.nativeTheme.shouldUseDarkColors, highContrast: true };
}
}
// ubuntu gnome seems to have 3 states, light dark and high contrast
else if (isLinux) {
if (electron.nativeTheme.shouldUseHighContrastColors) {
return { dark: true, highContrast: true };
}
}
return {
dark: electron.nativeTheme.shouldUseDarkColors,
highContrast: false
};
}
getPreferredBaseTheme(): ThemeTypeSelector | undefined {
const colorScheme = this.getColorScheme();
if (this.configurationService.getValue(ThemeSettings.DETECT_HC) && colorScheme.highContrast) {
return colorScheme.dark ? ThemeTypeSelector.HC_BLACK : ThemeTypeSelector.HC_LIGHT;
}
if (this.configurationService.getValue(ThemeSettings.DETECT_COLOR_SCHEME)) {
return colorScheme.dark ? ThemeTypeSelector.VS_DARK : ThemeTypeSelector.VS;
}
return undefined;
}
getBackgroundColor(): string {
const preferred = this.getPreferredBaseTheme();
const stored = this.getStoredBaseTheme();
// If the stored theme has the same base as the preferred, we can return the stored background
if (preferred === undefined || preferred === stored) {
const storedBackground = this.stateService.getItem<string | null>(THEME_BG_STORAGE_KEY, null);
if (storedBackground) {
return storedBackground;
}
}
// Otherwise we return the default background for the preferred base theme. If there's no preferred, use the stored one.
switch (preferred ?? stored) {
case ThemeTypeSelector.VS: return DEFAULT_BG_LIGHT;
case ThemeTypeSelector.HC_BLACK: return DEFAULT_BG_HC_BLACK;
case ThemeTypeSelector.HC_LIGHT: return DEFAULT_BG_HC_LIGHT;
default: return DEFAULT_BG_DARK;
}
}
private getStoredBaseTheme(): ThemeTypeSelector {
const baseTheme = this.stateService.getItem<ThemeTypeSelector>(THEME_STORAGE_KEY, ThemeTypeSelector.VS_DARK).split(' ')[0];
switch (baseTheme) {
case ThemeTypeSelector.VS: return ThemeTypeSelector.VS;
case ThemeTypeSelector.HC_BLACK: return ThemeTypeSelector.HC_BLACK;
case ThemeTypeSelector.HC_LIGHT: return ThemeTypeSelector.HC_LIGHT;
default: return ThemeTypeSelector.VS_DARK;
}
}
saveWindowSplash(windowId: number | undefined, workspace: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | undefined, splash: IPartsSplash): void {
// Update override as needed
const splashOverride = this.updateWindowSplashOverride(workspace, splash);
// Update in storage
this.stateService.setItems(coalesce([
{ key: THEME_STORAGE_KEY, data: splash.baseTheme },
{ key: THEME_BG_STORAGE_KEY, data: splash.colorInfo.background },
{ key: THEME_WINDOW_SPLASH_KEY, data: splash },
splashOverride ? { key: THEME_WINDOW_SPLASH_OVERRIDE_KEY, data: splashOverride } : undefined
]));
// Update in opened windows
if (typeof windowId === 'number') {
this.updateBackgroundColor(windowId, splash);
}
// Update system theme
this.updateSystemColorTheme();
}
private updateWindowSplashOverride(workspace: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | undefined, splash: IPartsSplash): IPartsSplashOverride | undefined {
let splashOverride: IPartsSplashOverride | undefined = undefined;
let changed = false;
if (workspace) {
splashOverride = { ...this.getWindowSplashOverride() }; // make a copy for modifications
changed = this.doUpdateWindowSplashOverride(workspace, splash, splashOverride, 'sideBar');
changed = this.doUpdateWindowSplashOverride(workspace, splash, splashOverride, 'auxiliaryBar') || changed;
}
return changed ? splashOverride : undefined;
}
private doUpdateWindowSplashOverride(workspace: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier, splash: IPartsSplash, splashOverride: IPartsSplashOverride, part: 'sideBar' | 'auxiliaryBar'): boolean {
const currentWidth = part === 'sideBar' ? splash.layoutInfo?.sideBarWidth : splash.layoutInfo?.auxiliarySideBarWidth;
const overrideWidth = part === 'sideBar' ? splashOverride.layoutInfo.sideBarWidth : splashOverride.layoutInfo.auxiliaryBarWidth;
// No layout info: remove override
let changed = false;
if (typeof currentWidth !== 'number') {
if (splashOverride.layoutInfo.workspaces[workspace.id]) {
delete splashOverride.layoutInfo.workspaces[workspace.id];
changed = true;
}
return changed;
}
let workspaceOverride = splashOverride.layoutInfo.workspaces[workspace.id];
if (!workspaceOverride) {
const workspaceEntries = Object.keys(splashOverride.layoutInfo.workspaces);
if (workspaceEntries.length >= ThemeMainService.WORKSPACE_OVERRIDE_LIMIT) {
delete splashOverride.layoutInfo.workspaces[workspaceEntries[0]];
changed = true;
}
workspaceOverride = { sideBarVisible: false, auxiliaryBarVisible: false };
splashOverride.layoutInfo.workspaces[workspace.id] = workspaceOverride;
changed = true;
}
// Part has width: update width & visibility override
if (currentWidth > 0) {
if (overrideWidth !== currentWidth) {
splashOverride.layoutInfo[part === 'sideBar' ? 'sideBarWidth' : 'auxiliaryBarWidth'] = currentWidth;
changed = true;
}
switch (part) {
case 'sideBar':
if (!workspaceOverride.sideBarVisible) {
workspaceOverride.sideBarVisible = true;
changed = true;
}
break;
case 'auxiliaryBar':
if (!workspaceOverride.auxiliaryBarVisible) {
workspaceOverride.auxiliaryBarVisible = true;
changed = true;
}
break;
}
}
// Part is hidden: update visibility override
else {
switch (part) {
case 'sideBar':
if (workspaceOverride.sideBarVisible) {
workspaceOverride.sideBarVisible = false;
changed = true;
}
break;
case 'auxiliaryBar':
if (workspaceOverride.auxiliaryBarVisible) {
workspaceOverride.auxiliaryBarVisible = false;
changed = true;
}
break;
}
}
return changed;
}
private updateBackgroundColor(windowId: number, splash: IPartsSplash): void {
for (const window of getAllWindowsExcludingOffscreen()) {
if (window.id === windowId) {
window.setBackgroundColor(splash.colorInfo.background);
break;
}
}
}
getWindowSplash(workspace: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | undefined): IPartsSplash | undefined {
try {
return this.doGetWindowSplash(workspace);
} catch (error) {
this.logService.error('[theme main service] Failed to get window splash', error);
return undefined;
}
}
private doGetWindowSplash(workspace: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | undefined): IPartsSplash | undefined {
const partSplash = this.stateService.getItem<IPartsSplash>(THEME_WINDOW_SPLASH_KEY);
if (!partSplash?.layoutInfo) {
return partSplash; // return early: overrides currently only apply to layout info
}
const override = this.getWindowSplashOverride();
// Figure out side bar width based on workspace and overrides
let sideBarWidth: number;
if (workspace) {
if (override.layoutInfo.workspaces[workspace.id]?.sideBarVisible === false) {
sideBarWidth = 0;
} else {
sideBarWidth = override.layoutInfo.sideBarWidth || partSplash.layoutInfo.sideBarWidth || ThemeMainService.DEFAULT_BAR_WIDTH;
}
} else {
sideBarWidth = 0;
}
// Figure out auxiliary bar width based on workspace, configuration and overrides
const auxiliarySideBarDefaultVisibility = this.configurationService.getValue(AUXILIARYBAR_DEFAULT_VISIBILITY);
let auxiliarySideBarWidth: number;
if (workspace) {
const auxiliaryBarVisible = override.layoutInfo.workspaces[workspace.id]?.auxiliaryBarVisible;
if (auxiliaryBarVisible === true) {
auxiliarySideBarWidth = override.layoutInfo.auxiliaryBarWidth || partSplash.layoutInfo.auxiliarySideBarWidth || ThemeMainService.DEFAULT_BAR_WIDTH;
} else if (auxiliaryBarVisible === false) {
auxiliarySideBarWidth = 0;
} else {
if (auxiliarySideBarDefaultVisibility === 'visible' || auxiliarySideBarDefaultVisibility === 'visibleInWorkspace') {
auxiliarySideBarWidth = override.layoutInfo.auxiliaryBarWidth || partSplash.layoutInfo.auxiliarySideBarWidth || ThemeMainService.DEFAULT_BAR_WIDTH;
} else {
auxiliarySideBarWidth = 0;
}
}
} else {
auxiliarySideBarWidth = 0; // technically not true if configured 'visible', but we never store splash per empty window, so we decide on a default here
}
return {
...partSplash,
layoutInfo: {
...partSplash.layoutInfo,
sideBarWidth,
auxiliarySideBarWidth
}
};
}
private getWindowSplashOverride(): IPartsSplashOverride {
let override = this.stateService.getItem<IPartsSplashOverride>(THEME_WINDOW_SPLASH_OVERRIDE_KEY);
if (!override?.layoutInfo) {
override = {
layoutInfo: {
sideBarWidth: ThemeMainService.DEFAULT_BAR_WIDTH,
auxiliaryBarWidth: ThemeMainService.DEFAULT_BAR_WIDTH,
workspaces: {}
}
};
}
if (!override.layoutInfo.sideBarWidth) {
override.layoutInfo.sideBarWidth = ThemeMainService.DEFAULT_BAR_WIDTH;
}
if (!override.layoutInfo.auxiliaryBarWidth) {
override.layoutInfo.auxiliaryBarWidth = ThemeMainService.DEFAULT_BAR_WIDTH;
}
if (!override.layoutInfo.workspaces) {
override.layoutInfo.workspaces = {};
}
return override;
}
}
@@ -12,6 +12,7 @@ import { revive } from '../../../base/common/marshalling.js';
import { escapeRegExpCharacters } from '../../../base/common/strings.js';
import { ThemeIcon } from '../../../base/common/themables.js';
import { URI, UriComponents } from '../../../base/common/uri.js';
import { Schemas } from '../../../base/common/network.js';
import { Position } from '../../../editor/common/core/position.js';
import { Range } from '../../../editor/common/core/range.js';
import { getWordAtText } from '../../../editor/common/core/wordHelper.js';
@@ -23,7 +24,6 @@ import { IInstantiationService } from '../../../platform/instantiation/common/in
import { ILogService } from '../../../platform/log/common/log.js';
import { IUriIdentityService } from '../../../platform/uriIdentity/common/uriIdentity.js';
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 { IChatAgentHistoryEntry, IChatAgentImplementation, IChatAgentRequest, IChatAgentService } from '../../contrib/chat/common/chatAgents.js';
import { IChatEditingService, IChatRelatedFileProviderMetadata } from '../../contrib/chat/common/chatEditingService.js';
@@ -313,7 +313,7 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA
};
this._agentIdsToCompletionProviders.set(id, this._chatAgentService.registerAgentCompletionProvider(id, provide));
this._agentCompletionProviders.set(handle, this._languageFeaturesService.completionProvider.register({ scheme: ChatInputPart.INPUT_SCHEME, hasAccessToAllModels: true }, {
this._agentCompletionProviders.set(handle, this._languageFeaturesService.completionProvider.register({ scheme: Schemas.vscodeChatInput, hasAccessToAllModels: true }, {
_debugDisplayName: 'chatAgentCompletions:' + handle,
triggerCharacters,
provideCompletionItems: async (model: ITextModel, position: Position, _context: CompletionContext, token: CancellationToken) => {
@@ -40,7 +40,7 @@ import { DEFAULT_EDITOR_ASSOCIATION, SaveReason } from '../../common/editor.js';
import { IViewBadge } from '../../common/views.js';
import { IChatAgentRequest, IChatAgentResult } from '../../contrib/chat/common/chatAgents.js';
import { IChatRequestDraft } from '../../contrib/chat/common/chatEditingService.js';
import { IChatRequestVariableEntry, isImageVariableEntry } from '../../contrib/chat/common/chatModel.js';
import { IChatRequestVariableEntry, isImageVariableEntry } from '../../contrib/chat/common/chatVariableEntries.js';
import { IChatAgentMarkdownContentWithVulnerability, IChatCodeCitation, IChatCommandButton, IChatConfirmation, IChatContentInlineReference, IChatContentReference, IChatExtensionsContent, IChatFollowup, IChatMarkdownContent, IChatMoveMessage, IChatPrepareToolInvocationPart, IChatProgressMessage, IChatResponseCodeblockUriPart, IChatTaskDto, IChatTaskResult, IChatTextEdit, IChatTreeData, IChatUserActionEvent, IChatWarningMessage } from '../../contrib/chat/common/chatService.js';
import { IToolData, IToolResult } from '../../contrib/chat/common/languageModelToolsService.js';
import * as chatProvider from '../../contrib/chat/common/languageModels.js';
@@ -59,7 +59,7 @@ import { CopilotUsageExtensionFeatureId } from '../../common/languageModelStats.
import { ILanguageModelToolsService } from '../../common/languageModelToolsService.js';
import { ChatViewId, IChatWidget, IChatWidgetService, showChatView, showCopilotView } from '../chat.js';
import { IChatEditorOptions } from '../chatEditor.js';
import { ChatEditorInput } from '../chatEditorInput.js';
import { ChatEditorInput, shouldShowClearEditingSessionConfirmation, showClearEditingSessionConfirmation } from '../chatEditorInput.js';
import { ChatViewPane } from '../chatViewPane.js';
import { convertBufferToScreenshotVariable } from '../contrib/screenshot.js';
import { clearChatEditor } from './chatClear.js';
@@ -990,52 +990,6 @@ export interface IClearEditingSessionConfirmationOptions {
messageOverride?: string;
}
export async function showClearEditingSessionConfirmation(editingSession: IChatEditingSession, dialogService: IDialogService, options?: IClearEditingSessionConfirmationOptions): Promise<boolean> {
const defaultPhrase = localize('chat.startEditing.confirmation.pending.message.default', "Starting a new chat will end your current edit session.");
const defaultTitle = localize('chat.startEditing.confirmation.title', "Start new chat?");
const phrase = options?.messageOverride ?? defaultPhrase;
const title = options?.titleOverride ?? defaultTitle;
const currentEdits = editingSession.entries.get();
const undecidedEdits = currentEdits.filter((edit) => edit.state.get() === ModifiedFileEntryState.Modified);
const { result } = await dialogService.prompt({
title,
message: phrase + ' ' + localize('chat.startEditing.confirmation.pending.message.2', "Do you want to keep pending edits to {0} files?", undecidedEdits.length),
type: 'info',
cancelButton: true,
buttons: [
{
label: localize('chat.startEditing.confirmation.acceptEdits', "Keep & Continue"),
run: async () => {
await editingSession.accept();
return true;
}
},
{
label: localize('chat.startEditing.confirmation.discardEdits', "Undo & Continue"),
run: async () => {
await editingSession.reject();
return true;
}
}
],
});
return Boolean(result);
}
export function shouldShowClearEditingSessionConfirmation(editingSession: IChatEditingSession): boolean {
const currentEdits = editingSession.entries.get();
const currentEditCount = currentEdits.length;
if (currentEditCount) {
const undecidedEdits = currentEdits.filter((edit) => edit.state.get() === ModifiedFileEntryState.Modified);
return !!undecidedEdits.length;
}
return false;
}
// --- Chat Submenus in various Components
@@ -23,7 +23,7 @@ import { FileEditorInput } from '../../../files/browser/editors/fileEditorInput.
import { NotebookEditorInput } from '../../../notebook/common/notebookEditorInput.js';
import { IChatContextPickService, IChatContextValueItem, IChatContextPickerItem, IChatContextPickerPickItem } from '../chatContextPickService.js';
import { IChatEditingService } from '../../common/chatEditingService.js';
import { IChatRequestToolEntry, IChatRequestToolSetEntry, IChatRequestVariableEntry, IImageVariableEntry, OmittedState } from '../../common/chatModel.js';
import { IChatRequestToolEntry, IChatRequestToolSetEntry, IChatRequestVariableEntry, IImageVariableEntry, OmittedState } from '../../common/chatVariableEntries.js';
import { IToolData, ToolDataSource, ToolSet } from '../../common/languageModelToolsService.js';
import { IChatWidget } from '../chat.js';
import { imageToHash, isImage } from '../chatPasteProviders.js';
@@ -37,7 +37,7 @@ import { isSearchTreeFileMatch, isSearchTreeMatch } from '../../../search/browse
import { ISymbolQuickPickItem, SymbolsQuickAccessProvider } from '../../../search/browser/symbolsQuickAccess.js';
import { SearchContext } from '../../../search/common/constants.js';
import { ChatContextKeys } from '../../common/chatContextKeys.js';
import { IChatRequestVariableEntry, OmittedState } from '../../common/chatModel.js';
import { IChatRequestVariableEntry, OmittedState } from '../../common/chatVariableEntries.js';
import { ChatAgentLocation } from '../../common/constants.js';
import { IChatWidget, IChatWidgetService, IQuickChatService, showChatView } from '../chat.js';
import { IChatContextPickerItem, IChatContextPickService, IChatContextValueItem, isChatContextPickerPickItem } from '../chatContextPickService.js';
@@ -17,10 +17,10 @@ import { ServicesAccessor } from '../../../../../platform/instantiation/common/i
import { IQuickInputButton, IQuickInputService, IQuickPickItem, IQuickPickSeparator } from '../../../../../platform/quickinput/common/quickInput.js';
import { IEditorService } from '../../../../services/editor/common/editorService.js';
import { IExtensionsWorkbenchService } from '../../../extensions/common/extensions.js';
import { AddConfigurationAction } from '../../../mcp/browser/mcpCommands.js';
import { McpCommandIds } from '../../../mcp/common/mcpCommandIds.js';
import { IMcpRegistry } from '../../../mcp/common/mcpRegistryTypes.js';
import { IMcpServer, IMcpService, McpConnectionState } from '../../../mcp/common/mcpTypes.js';
import { IToolData, ToolSet, ToolDataSource, ILanguageModelToolsService } from '../../common/languageModelToolsService.js';
import { ILanguageModelToolsService, IToolData, ToolDataSource, ToolSet } from '../../common/languageModelToolsService.js';
import { ConfigureToolSets } from '../tools/toolSetsContribution.js';
@@ -86,7 +86,7 @@ export async function showToolsPicker(
picked: false,
};
const addMcpPick: CallbackPick = { type: 'item', label: localize('addServer', "Add MCP Server..."), iconClass: ThemeIcon.asClassName(Codicon.add), pickable: false, run: () => commandService.executeCommand(AddConfigurationAction.ID) };
const addMcpPick: CallbackPick = { type: 'item', label: localize('addServer', "Add MCP Server..."), iconClass: ThemeIcon.asClassName(Codicon.add), pickable: false, run: () => commandService.executeCommand(McpCommandIds.AddConfiguration) };
const configureToolSetsPick: CallbackPick = { type: 'item', label: localize('configToolSet', "Configure Tool Sets..."), iconClass: ThemeIcon.asClassName(Codicon.gear), pickable: false, run: () => commandService.executeCommand(ConfigureToolSets.ID) };
const addExpPick: CallbackPick = { type: 'item', label: localize('addExtension', "Install Extension..."), iconClass: ThemeIcon.asClassName(Codicon.add), pickable: false, run: () => extensionWorkbenchService.openSearch('@tag:language-model-tools') };
const addPick: CallbackPick = {
@@ -24,7 +24,7 @@ import { FileKind, IFileService } from '../../../../../platform/files/common/fil
import { ILabelService } from '../../../../../platform/label/common/label.js';
import { ResourceLabels } from '../../../../browser/labels.js';
import { ResourceContextKey } from '../../../../common/contextkeys.js';
import { IChatRequestImplicitVariableEntry } from '../../common/chatModel.js';
import { IChatRequestImplicitVariableEntry } from '../../common/chatVariableEntries.js';
import { IChatWidgetService } from '../chat.js';
import { ChatAttachmentModel } from '../chatAttachmentModel.js';
@@ -96,7 +96,7 @@ import { ChatResponseAccessibleView } from './chatResponseAccessibleView.js';
import { ChatSetupContribution } from './chatSetup.js';
import { ChatStatusBarEntry } from './chatStatus.js';
import { ChatVariablesService } from './chatVariables.js';
import { ChatWidgetService } from './chatWidget.js';
import { ChatWidget, ChatWidgetService } from './chatWidget.js';
import { ChatCodeBlockContextProviderService } from './codeBlockContextProviderService.js';
import { ChatImplicitContextContribution } from './contrib/chatImplicitContext.js';
import './contrib/chatInputCompletions.js';
@@ -110,6 +110,7 @@ import product from '../../../../platform/product/common/product.js';
import { ChatModeService, IChatModeService } from '../common/chatModes.js';
import { ChatResponseResourceFileSystemProvider } from '../common/chatResponseResourceFileSystemProvider.js';
import { runSaveToPromptAction, SAVE_TO_PROMPT_SLASH_COMMAND_NAME } from './promptSyntax/saveToPromptAction.js';
import { ChatDynamicVariableModel } from './contrib/chatDynamicVariables.js';
// Register configuration
const configurationRegistry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration);
@@ -762,3 +763,5 @@ registerPromptFileContributions();
registerWorkbenchContribution2(UserToolSetsContributions.ID, UserToolSetsContributions, WorkbenchPhase.Eventually);
registerAction2(ConfigureToolSets);
ChatWidget.CONTRIBS.push(ChatDynamicVariableModel);
@@ -8,7 +8,7 @@ import { Emitter } from '../../../../base/common/event.js';
import { basename } from '../../../../base/common/resources.js';
import { IRange } from '../../../../editor/common/core/range.js';
import { Disposable } from '../../../../base/common/lifecycle.js';
import { IChatRequestFileEntry, IChatRequestVariableEntry } from '../common/chatModel.js';
import { IChatRequestFileEntry, IChatRequestVariableEntry } from '../common/chatVariableEntries.js';
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
import { ChatPromptAttachmentsCollection } from './chatAttachmentModel/chatPromptAttachmentsCollection.js';
import { IFileService } from '../../../../platform/files/common/files.js';
@@ -3,16 +3,16 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { URI } from '../../../../../base/common/uri.js';
import { Emitter } from '../../../../../base/common/event.js';
import { Disposable, DisposableMap } from '../../../../../base/common/lifecycle.js';
import { basename, isEqual } from '../../../../../base/common/resources.js';
import { ChatPromptAttachmentModel } from './chatPromptAttachmentModel.js';
import { URI } from '../../../../../base/common/uri.js';
import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
import { IChatRequestVariableEntry, IPromptVariableEntry, isChatRequestFileEntry } from '../../common/chatVariableEntries.js';
import { PromptsConfig } from '../../common/promptSyntax/config/config.js';
import { IPromptFileReference } from '../../common/promptSyntax/parsers/types.js';
import { Disposable, DisposableMap } from '../../../../../base/common/lifecycle.js';
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
import { IChatRequestVariableEntry, IPromptVariableEntry, isChatRequestFileEntry } from '../../common/chatModel.js';
import { ChatPromptAttachmentModel } from './chatPromptAttachmentModel.js';
/**
* Prefix for all prompt instruction variable IDs.
@@ -24,7 +24,8 @@ import { UntitledTextEditorInput } from '../../../services/untitled/common/untit
import { createNotebookOutputVariableEntry, NOTEBOOK_CELL_OUTPUT_MIME_TYPE_LIST_FOR_CHAT_CONST } from '../../notebook/browser/contrib/chat/notebookChatUtils.js';
import { getOutputViewModelFromId } from '../../notebook/browser/controller/cellOutputActions.js';
import { getNotebookEditorFromEditorPane } from '../../notebook/browser/notebookBrowser.js';
import { CHAT_ATTACHABLE_IMAGE_MIME_TYPES, getAttachableImageExtension, IChatRequestVariableEntry, IDiagnosticVariableEntry, IDiagnosticVariableEntryFilterData, ISymbolVariableEntry, OmittedState } from '../common/chatModel.js';
import { CHAT_ATTACHABLE_IMAGE_MIME_TYPES, getAttachableImageExtension } from '../common/chatModel.js';
import { IChatRequestVariableEntry, OmittedState, IDiagnosticVariableEntry, IDiagnosticVariableEntryFilterData, ISymbolVariableEntry } from '../common/chatVariableEntries.js';
import { imageToHash } from './chatPasteProviders.js';
import { resizeImage } from './imageUtils.js';
@@ -25,12 +25,11 @@ import { IOpenerService, OpenInternalOptions } from '../../../../platform/opener
import { IThemeService, FolderThemeIcon } from '../../../../platform/theme/common/themeService.js';
import { IResourceLabel, ResourceLabels, IFileLabelOptions } from '../../../browser/labels.js';
import { revealInSideBarCommand } from '../../files/browser/fileActions.contribution.js';
import { IChatRequestPasteVariableEntry, IChatRequestToolEntry, IChatRequestToolSetEntry, IChatRequestVariableEntry, IElementVariableEntry, INotebookOutputVariableEntry, ISCMHistoryItemVariableEntry, OmittedState } from '../common/chatModel.js';
import { IChatRequestPasteVariableEntry, IChatRequestToolEntry, IChatRequestToolSetEntry, IChatRequestVariableEntry, IElementVariableEntry, INotebookOutputVariableEntry, ISCMHistoryItemVariableEntry, OmittedState } from '../common/chatVariableEntries.js';
import { ILanguageModelChatMetadataAndIdentifier, ILanguageModelsService } from '../common/languageModels.js';
import { chatAttachmentResourceContextKey } from './chatContentParts/chatAttachmentsContentPart.js';
import { KeyCode } from '../../../../base/common/keyCodes.js';
import { basename, dirname } from '../../../../base/common/path.js';
import { IContextKey, IContextKeyService, IScopedContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
import { IContextKey, IContextKeyService, IScopedContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js';
import { IMenuService, MenuId } from '../../../../platform/actions/common/actions.js';
import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';
import { INotebookService } from '../../notebook/common/notebookService.js';
@@ -815,3 +814,5 @@ function addBasicContextMenu(accessor: ServicesAccessor, widget: HTMLElement, sc
});
});
}
export const chatAttachmentResourceContextKey = new RawContextKey<string>('chatAttachmentResource', undefined, { type: 'URI', description: localize('resource', "The full value of the chat attachment resource, including scheme and path") });
@@ -10,17 +10,12 @@ import { Disposable, DisposableStore } from '../../../../../base/common/lifecycl
import { basename } from '../../../../../base/common/path.js';
import { URI } from '../../../../../base/common/uri.js';
import { Range } from '../../../../../editor/common/core/range.js';
import { localize } from '../../../../../nls.js';
import { RawContextKey } from '../../../../../platform/contextkey/common/contextkey.js';
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
import { ResourceLabels } from '../../../../browser/labels.js';
import { IChatRequestVariableEntry, isElementVariableEntry, isImageVariableEntry, isNotebookOutputVariableEntry, isPasteVariableEntry, isSCMHistoryItemVariableEntry, OmittedState } from '../../common/chatModel.js';
import { IChatRequestVariableEntry, isElementVariableEntry, isImageVariableEntry, isNotebookOutputVariableEntry, isPasteVariableEntry, isSCMHistoryItemVariableEntry, OmittedState } from '../../common/chatVariableEntries.js';
import { ChatResponseReferencePartStatusKind, IChatContentReference } from '../../common/chatService.js';
import { DefaultChatAttachmentWidget, ElementChatAttachmentWidget, FileAttachmentWidget, ImageAttachmentWidget, NotebookCellOutputChatAttachmentWidget, PasteAttachmentWidget, SCMHistoryItemAttachmentWidget, ToolSetOrToolItemAttachmentWidget } from '../chatAttachmentWidgets.js';
export const chatAttachmentResourceContextKey = new RawContextKey<string>('chatAttachmentResource', undefined, { type: 'URI', description: localize('resource', "The full value of the chat attachment resource, including scheme and path") });
export class ChatAttachmentsContentPart extends Disposable {
private readonly attachedContextDisposables = this._register(new DisposableStore());
@@ -3,6 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { $ } from '../../../../../base/browser/dom.js';
import { ButtonWithIcon } from '../../../../../base/browser/ui/button/button.js';
import { Codicon } from '../../../../../base/common/codicons.js';
import { Emitter } from '../../../../../base/common/event.js';
@@ -13,7 +14,6 @@ import { localize } from '../../../../../nls.js';
import { IChatRendererContent } from '../../common/chatViewModel.js';
import { ChatTreeItem } from '../chat.js';
import { IChatContentPart, IChatContentPartRenderContext } from './chatContentParts.js';
import { $ } from './chatReferencesContentPart.js';
export abstract class ChatCollapsibleContentPart extends Disposable implements IChatContentPart {
@@ -46,7 +46,7 @@ import { ChatCollapsibleContentPart } from './chatCollapsibleContentPart.js';
import { IDisposableReference, ResourcePool } from './chatCollections.js';
import { IChatContentPartRenderContext } from './chatContentParts.js';
export const $ = dom.$;
const $ = dom.$;
export interface IChatReferenceListItem extends IChatContentReference {
title?: string;
@@ -31,7 +31,8 @@ import { INotificationService } from '../../../../../platform/notification/commo
import { IProgressService, ProgressLocation } from '../../../../../platform/progress/common/progress.js';
import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js';
import { REVEAL_IN_EXPLORER_COMMAND_ID } from '../../../files/browser/fileConstants.js';
import { getAttachableImageExtension, IChatRequestVariableEntry } from '../../common/chatModel.js';
import { getAttachableImageExtension } from '../../common/chatModel.js';
import { IChatRequestVariableEntry } from '../../common/chatVariableEntries.js';
import { IChatRendererContent } from '../../common/chatViewModel.js';
import { ChatTreeItem, IChatCodeBlockInfo } from '../chat.js';
import { CodeBlockPart, ICodeBlockData, ICodeBlockRenderOptions } from '../codeBlockPart.js';
@@ -10,7 +10,7 @@ import { ThemeIcon } from '../../../../base/common/themables.js';
import { isObject } from '../../../../base/common/types.js';
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
import { IQuickPickSeparator } from '../../../../platform/quickinput/common/quickInput.js';
import { IChatRequestVariableEntry } from '../common/chatModel.js';
import { IChatRequestVariableEntry } from '../common/chatVariableEntries.js';
import { IChatWidget } from './chat.js';
@@ -23,7 +23,7 @@ import { IThemeService, Themable } from '../../../../platform/theme/common/theme
import { ISharedWebContentExtractorService } from '../../../../platform/webContentExtractor/common/webContentExtractor.js';
import { IEditorService } from '../../../services/editor/common/editorService.js';
import { IExtensionService, isProposedApiEnabled } from '../../../services/extensions/common/extensions.js';
import { IChatRequestVariableEntry } from '../common/chatModel.js';
import { IChatRequestVariableEntry } from '../common/chatVariableEntries.js';
import { IChatWidgetService } from './chat.js';
import { ImageTransferData, resolveEditorAttachContext, resolveImageAttachContext, resolveMarkerAttachContext, resolveNotebookOutputAttachContext, resolveSymbolsAttachContext } from './chatAttachmentResolve.js';
import { ChatAttachmentModel } from './chatAttachmentModel.js';
@@ -4,12 +4,12 @@
*--------------------------------------------------------------------------------------------*/
import { Disposable } from '../../../../base/common/lifecycle.js';
import { Schemas } from '../../../../base/common/network.js';
import { URI } from '../../../../base/common/uri.js';
import { ILanguageService } from '../../../../editor/common/languages/language.js';
import { ITextModel } from '../../../../editor/common/model.js';
import { IModelService } from '../../../../editor/common/services/model.js';
import { ITextModelContentProvider, ITextModelService } from '../../../../editor/common/services/resolverService.js';
import { ChatInputPart } from './chatInputPart.js';
export class ChatInputBoxContentProvider extends Disposable implements ITextModelContentProvider {
@@ -19,7 +19,7 @@ export class ChatInputBoxContentProvider extends Disposable implements ITextMode
@ILanguageService private readonly languageService: ILanguageService
) {
super();
this._register(textModelService.registerTextModelContentProvider(ChatInputPart.INPUT_SCHEME, this));
this._register(textModelService.registerTextModelContentProvider(Schemas.vscodeChatInput, this));
}
async provideTextContent(resource: URI): Promise<ITextModel | null> {
@@ -41,11 +41,11 @@ import { IEditorPane, SaveReason } from '../../../../common/editor.js';
import { IFilesConfigurationService } from '../../../../services/filesConfiguration/common/filesConfigurationService.js';
import { ITextFileService, isTextFileEditorModel, stringToSnapshot } from '../../../../services/textfile/common/textfiles.js';
import { ICellEditOperation } from '../../../notebook/common/notebookCommon.js';
import { ChatEditKind, IModifiedFileEntry, IModifiedFileEntryEditorIntegration, ModifiedFileEntryState } from '../../common/chatEditingService.js';
import { ChatEditKind, IModifiedEntryTelemetryInfo, IModifiedFileEntry, IModifiedFileEntryEditorIntegration, ISnapshotEntry, ModifiedFileEntryState } from '../../common/chatEditingService.js';
import { IChatResponseModel } from '../../common/chatModel.js';
import { IChatService } from '../../common/chatService.js';
import { ChatEditingCodeEditorIntegration, IDocumentDiff2 } from './chatEditingCodeEditorIntegration.js';
import { AbstractChatEditingModifiedFileEntry, IModifiedEntryTelemetryInfo, ISnapshotEntry, pendingRewriteMinimap } from './chatEditingModifiedFileEntry.js';
import { AbstractChatEditingModifiedFileEntry, pendingRewriteMinimap } from './chatEditingModifiedFileEntry.js';
import { ChatEditingSnapshotTextModelContentProvider, ChatEditingTextModelContentProvider } from './chatEditingTextModelContentProviders.js';
@@ -10,7 +10,6 @@ import { Schemas } from '../../../../../base/common/network.js';
import { clamp } from '../../../../../base/common/numbers.js';
import { autorun, derived, IObservable, ITransaction, observableValue, observableValueOpts, transaction } from '../../../../../base/common/observable.js';
import { URI } from '../../../../../base/common/uri.js';
import { StringEdit } from '../../../../../editor/common/core/edits/stringEdit.js';
import { TextEdit } from '../../../../../editor/common/languages.js';
import { localize } from '../../../../../nls.js';
import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
@@ -22,8 +21,7 @@ import { IUndoRedoElement, IUndoRedoService } from '../../../../../platform/undo
import { IEditorPane } from '../../../../common/editor.js';
import { IFilesConfigurationService } from '../../../../services/filesConfiguration/common/filesConfigurationService.js';
import { ICellEditOperation } from '../../../notebook/common/notebookCommon.js';
import { IChatAgentResult } from '../../common/chatAgents.js';
import { ChatEditKind, IModifiedFileEntry, IModifiedFileEntryEditorIntegration, ModifiedFileEntryState } from '../../common/chatEditingService.js';
import { ChatEditKind, IModifiedEntryTelemetryInfo, IModifiedFileEntry, IModifiedFileEntryEditorIntegration, ISnapshotEntry, ModifiedFileEntryState } from '../../common/chatEditingService.js';
import { IChatResponseModel } from '../../common/chatModel.js';
import { IChatService } from '../../common/chatService.js';
@@ -318,22 +316,3 @@ export abstract class AbstractChatEditingModifiedFileEntry extends Disposable im
abstract initialContent: string;
}
export interface IModifiedEntryTelemetryInfo {
readonly agentId: string | undefined;
readonly command: string | undefined;
readonly sessionId: string;
readonly requestId: string;
readonly result: IChatAgentResult | undefined;
}
export interface ISnapshotEntry {
readonly resource: URI;
readonly languageId: string;
readonly snapshotUri: URI;
readonly original: string;
readonly current: string;
readonly originalToCurrentEdit: StringEdit;
readonly state: ModifiedFileEntryState;
telemetryInfo: IModifiedEntryTelemetryInfo;
}
@@ -43,10 +43,10 @@ import { INotebookEditorModelResolverService } from '../../../notebook/common/no
import { INotebookLoggingService } from '../../../notebook/common/notebookLoggingService.js';
import { INotebookService } from '../../../notebook/common/notebookService.js';
import { INotebookEditorWorkerService } from '../../../notebook/common/services/notebookWorkerService.js';
import { ChatEditKind, IModifiedFileEntryEditorIntegration, ModifiedFileEntryState } from '../../common/chatEditingService.js';
import { ChatEditKind, IModifiedEntryTelemetryInfo, IModifiedFileEntryEditorIntegration, ISnapshotEntry, ModifiedFileEntryState } from '../../common/chatEditingService.js';
import { IChatResponseModel } from '../../common/chatModel.js';
import { IChatService } from '../../common/chatService.js';
import { AbstractChatEditingModifiedFileEntry, IModifiedEntryTelemetryInfo, ISnapshotEntry } from './chatEditingModifiedFileEntry.js';
import { AbstractChatEditingModifiedFileEntry } from './chatEditingModifiedFileEntry.js';
import { createSnapshot, deserializeSnapshot, getNotebookSnapshotFileURI, restoreSnapshot, SnapshotComparer } from './notebook/chatEditingModifiedNotebookSnapshot.js';
import { ChatEditingNewNotebookContentEdits } from './notebook/chatEditingNewNotebookContentEdits.js';
import { ChatEditingNotebookCellEntry } from './notebook/chatEditingNotebookCellEntry.js';
@@ -35,11 +35,11 @@ import { MultiDiffEditor } from '../../../multiDiffEditor/browser/multiDiffEdito
import { MultiDiffEditorInput } from '../../../multiDiffEditor/browser/multiDiffEditorInput.js';
import { CellUri, ICellEditOperation } from '../../../notebook/common/notebookCommon.js';
import { INotebookService } from '../../../notebook/common/notebookService.js';
import { ChatEditingSessionState, ChatEditKind, getMultiDiffSourceUri, IChatEditingSession, IEditSessionEntryDiff, IModifiedFileEntry, IStreamingEdits, ModifiedFileEntryState } from '../../common/chatEditingService.js';
import { ChatEditingSessionState, ChatEditKind, getMultiDiffSourceUri, IChatEditingSession, IEditSessionEntryDiff, IModifiedEntryTelemetryInfo, IModifiedFileEntry, ISnapshotEntry, IStreamingEdits, ModifiedFileEntryState } from '../../common/chatEditingService.js';
import { IChatRequestDisablement, IChatResponseModel } from '../../common/chatModel.js';
import { IChatService } from '../../common/chatService.js';
import { ChatEditingModifiedDocumentEntry } from './chatEditingModifiedDocumentEntry.js';
import { AbstractChatEditingModifiedFileEntry, IModifiedEntryTelemetryInfo, ISnapshotEntry } from './chatEditingModifiedFileEntry.js';
import { AbstractChatEditingModifiedFileEntry } from './chatEditingModifiedFileEntry.js';
import { ChatEditingModifiedNotebookEntry } from './chatEditingModifiedNotebookEntry.js';
import { ChatEditingSessionStorage, IChatEditingSessionSnapshot, IChatEditingSessionStop, StoredSessionState } from './chatEditingSessionStorage.js';
import { ChatEditingTextModelContentProvider } from './chatEditingTextModelContentProviders.js';
@@ -13,8 +13,7 @@ import { IEnvironmentService } from '../../../../../platform/environment/common/
import { IFileService } from '../../../../../platform/files/common/files.js';
import { ILogService } from '../../../../../platform/log/common/log.js';
import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js';
import { ISnapshotEntry } from './chatEditingModifiedFileEntry.js';
import { WorkingSetDisplayMetadata, ModifiedFileEntryState } from '../../common/chatEditingService.js';
import { WorkingSetDisplayMetadata, ModifiedFileEntryState, ISnapshotEntry } from '../../common/chatEditingService.js';
const STORAGE_CONTENTS_FOLDER = 'contents';
const STORAGE_STATE_FILE = 'state.json';
@@ -8,7 +8,6 @@ import { ITextModel } from '../../../../../editor/common/model.js';
import { IModelService } from '../../../../../editor/common/services/model.js';
import { ITextModelContentProvider } from '../../../../../editor/common/services/resolverService.js';
import { chatEditingSnapshotScheme, IChatEditingService } from '../../common/chatEditingService.js';
import { ChatEditingSession } from './chatEditingSession.js';
type ChatEditingTextModelContentQueryData = { kind: 'doc'; documentId: string; chatSessionId: string };
@@ -72,7 +71,7 @@ export class ChatEditingSnapshotTextModelContentProvider implements ITextModelCo
const data: ChatEditingSnapshotTextModelContentQueryData = JSON.parse(resource.query);
const session = this._chatEditingService.getEditingSession(data.sessionId);
if (!(session instanceof ChatEditingSession) || !data.requestId) {
if (!session || !data.requestId) {
return null;
}
@@ -8,8 +8,8 @@ import { computeDiff } from '../../../../notebook/common/notebookDiff.js';
import { INotebookEditorModelResolverService } from '../../../../notebook/common/notebookEditorModelResolverService.js';
import { INotebookLoggingService } from '../../../../notebook/common/notebookLoggingService.js';
import { INotebookEditorWorkerService } from '../../../../notebook/common/services/notebookWorkerService.js';
import { IEditSessionEntryDiff } from '../../../common/chatEditingService.js';
import { ISnapshotEntry } from '../chatEditingModifiedFileEntry.js';
import { IEditSessionEntryDiff, ISnapshotEntry } from '../../../common/chatEditingService.js';
export class ChatEditingModifiedNotebookDiff {
static NewModelCounter: number = 0;
@@ -16,7 +16,6 @@ import { IWorkbenchContribution } from '../../../../../common/contributions.js';
import { INotebookService } from '../../../../notebook/common/notebookService.js';
import { IChatEditingService } from '../../../common/chatEditingService.js';
import { ChatEditingNotebookSnapshotScheme, deserializeSnapshot } from './chatEditingModifiedNotebookSnapshot.js';
import { ChatEditingSession } from '../chatEditingSession.js';
export class ChatEditingNotebookFileSystemProviderContrib extends Disposable implements IWorkbenchContribution {
@@ -89,7 +88,7 @@ export class ChatEditingNotebookFileSystemProvider implements IFileSystemProvide
throw new Error('File not found, viewType not found');
}
const session = this._chatEditingService.getEditingSession(queryData.sessionId);
if (!(session instanceof ChatEditingSession) || !queryData.requestId) {
if (!session || !queryData.requestId) {
throw new Error('File not found, session not found');
}
const snapshotEntry = session.getSnapshot(queryData.requestId, queryData.undoStop || undefined, resource);
@@ -31,7 +31,7 @@ import { IFileService } from '../../../../../platform/files/common/files.js';
import { IEnvironmentService } from '../../../../../platform/environment/common/environment.js';
import { URI } from '../../../../../base/common/uri.js';
import { ILogService } from '../../../../../platform/log/common/log.js';
import { IChatRequestVariableEntry } from '../../common/chatModel.js';
import { IChatRequestVariableEntry } from '../../common/chatVariableEntries.js';
import { IPreferencesService } from '../../../../services/preferences/common/preferences.js';
import { IBrowserElementsService } from '../../../../services/browserElements/browser/browserElementsService.js';
import { IContextMenuService } from '../../../../../platform/contextview/browser/contextView.js';
@@ -20,7 +20,8 @@ import { IChatModel } from '../common/chatModel.js';
import { IChatService } from '../common/chatService.js';
import { ChatAgentLocation } from '../common/constants.js';
import { ConfirmResult, IDialogService } from '../../../../platform/dialogs/common/dialogs.js';
import { shouldShowClearEditingSessionConfirmation, showClearEditingSessionConfirmation } from './actions/chatActions.js';
import { IChatEditingSession, ModifiedFileEntryState } from '../common/chatEditingService.js';
import { IClearEditingSessionConfirmationOptions } from './actions/chatActions.js';
const ChatEditorIcon = registerIcon('chat-editor-label-icon', Codicon.commentDiscussion, nls.localize('chatEditorLabelIcon', 'Icon of the chat editor label.'));
@@ -231,3 +232,50 @@ export class ChatEditorInputSerializer implements IEditorSerializer {
}
}
}
export async function showClearEditingSessionConfirmation(editingSession: IChatEditingSession, dialogService: IDialogService, options?: IClearEditingSessionConfirmationOptions): Promise<boolean> {
const defaultPhrase = nls.localize('chat.startEditing.confirmation.pending.message.default1', "Starting a new chat will end your current edit session.");
const defaultTitle = nls.localize('chat.startEditing.confirmation.title', "Start new chat?");
const phrase = options?.messageOverride ?? defaultPhrase;
const title = options?.titleOverride ?? defaultTitle;
const currentEdits = editingSession.entries.get();
const undecidedEdits = currentEdits.filter((edit) => edit.state.get() === ModifiedFileEntryState.Modified);
const { result } = await dialogService.prompt({
title,
message: phrase + ' ' + nls.localize('chat.startEditing.confirmation.pending.message.2', "Do you want to keep pending edits to {0} files?", undecidedEdits.length),
type: 'info',
cancelButton: true,
buttons: [
{
label: nls.localize('chat.startEditing.confirmation.acceptEdits', "Keep & Continue"),
run: async () => {
await editingSession.accept();
return true;
}
},
{
label: nls.localize('chat.startEditing.confirmation.discardEdits', "Undo & Continue"),
run: async () => {
await editingSession.reject();
return true;
}
}
],
});
return Boolean(result);
}
export function shouldShowClearEditingSessionConfirmation(editingSession: IChatEditingSession): boolean {
const currentEdits = editingSession.entries.get();
const currentEditCount = currentEdits.length;
if (currentEditCount) {
const undecidedEdits = currentEdits.filter((edit) => edit.state.get() === ModifiedFileEntryState.Modified);
return !!undecidedEdits.length;
}
return false;
}
@@ -41,8 +41,7 @@ import { ExplorerFolderContext } from '../../files/common/files.js';
import { IWorkspaceSymbol } from '../../search/common/search.js';
import { IChatContentInlineReference } from '../common/chatService.js';
import { IChatWidgetService } from './chat.js';
import { hookUpSymbolAttachmentDragAndContextMenu } from './chatAttachmentWidgets.js';
import { chatAttachmentResourceContextKey } from './chatContentParts/chatAttachmentsContentPart.js';
import { chatAttachmentResourceContextKey, hookUpSymbolAttachmentDragAndContextMenu } from './chatAttachmentWidgets.js';
import { IChatMarkdownAnchorService } from './chatContentParts/chatMarkdownAnchorService.js';
type ContentRefData =
@@ -27,6 +27,7 @@ import { isMacintosh } from '../../../../base/common/platform.js';
import { isEqual } from '../../../../base/common/resources.js';
import { assertType } from '../../../../base/common/types.js';
import { URI } from '../../../../base/common/uri.js';
import { Schemas } from '../../../../base/common/network.js';
import { IEditorConstructionOptions } from '../../../../editor/browser/config/editorConfiguration.js';
import { EditorExtensionsRegistry } from '../../../../editor/browser/editorExtensions.js';
import { CodeEditorWidget } from '../../../../editor/browser/widget/codeEditor/codeEditorWidget.js';
@@ -75,7 +76,7 @@ import { IChatAgentService } from '../common/chatAgents.js';
import { ChatContextKeys } from '../common/chatContextKeys.js';
import { IChatEditingSession } from '../common/chatEditingService.js';
import { ChatEntitlement, IChatEntitlementService } from '../common/chatEntitlementService.js';
import { IChatRequestVariableEntry, isElementVariableEntry, isImageVariableEntry, isNotebookOutputVariableEntry, isPasteVariableEntry, isSCMHistoryItemVariableEntry } from '../common/chatModel.js';
import { IChatRequestVariableEntry, isElementVariableEntry, isImageVariableEntry, isNotebookOutputVariableEntry, isPasteVariableEntry, isSCMHistoryItemVariableEntry } from '../common/chatVariableEntries.js';
import { ChatMode2, IChatMode, IChatModeService, isBuiltinChatMode, validateChatMode2 } from '../common/chatModes.js';
import { IChatFollowup } from '../common/chatService.js';
import { IChatVariablesService } from '../common/chatVariables.js';
@@ -137,7 +138,6 @@ export interface IWorkingSetEntry {
const GlobalLastChatModeKey = 'chat.lastChatMode';
export class ChatInputPart extends Disposable implements IHistoryNavigationWidget {
static readonly INPUT_SCHEME = 'chatSessionInput';
private static _counter = 0;
private _onDidLoadInputState: Emitter<IChatInputState | undefined>;
@@ -427,7 +427,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
this._onDidChangeCurrentChatMode = this._register(new Emitter<void>());
this.onDidChangeCurrentChatMode = this._onDidChangeCurrentChatMode.event;
this._currentMode = ChatMode2.Ask;
this.inputUri = URI.parse(`${ChatInputPart.INPUT_SCHEME}:input-${ChatInputPart._counter++}`);
this.inputUri = URI.parse(`${Schemas.vscodeChatInput}:input-${ChatInputPart._counter++}`);
this._chatEditsActionsDisposables = this._register(new DisposableStore());
this._chatEditsDisposables = this._register(new DisposableStore());
this._attemptedWorkingSetEntriesCount = 0;
@@ -47,7 +47,7 @@ import { annotateSpecialMarkdownContent } from '../common/annotations.js';
import { checkModeOption } from '../common/chat.js';
import { IChatAgentMetadata } from '../common/chatAgents.js';
import { ChatContextKeys } from '../common/chatContextKeys.js';
import { IChatRequestVariableEntry, IChatTextEditGroup } from '../common/chatModel.js';
import { IChatTextEditGroup } from '../common/chatModel.js';
import { chatSubcommandLeader } from '../common/chatParserTypes.js';
import { ChatAgentVoteDirection, ChatAgentVoteDownReason, ChatErrorLevel, IChatConfirmation, IChatContentReference, IChatExtensionsContent, IChatFollowup, IChatMarkdownContent, IChatTask, IChatTaskSerialized, IChatToolInvocation, IChatToolInvocationSerialized, IChatTreeData, IChatUndoStop } from '../common/chatService.js';
import { IChatCodeCitations, IChatErrorDetailsPart, IChatReferences, IChatRendererContent, IChatRequestViewModel, IChatResponseViewModel, IChatWorkingProgress, isRequestVM, isResponseVM } from '../common/chatViewModel.js';
@@ -79,6 +79,7 @@ import { ChatMarkdownRenderer } from './chatMarkdownRenderer.js';
import { ChatEditorOptions } from './chatOptions.js';
import { ChatCodeBlockContentProvider, CodeBlockPart } from './codeBlockPart.js';
import { canceledName } from '../../../../base/common/errors.js';
import { IChatRequestVariableEntry } from '../common/chatVariableEntries.js';
const $ = dom.$;
@@ -9,6 +9,7 @@ import { HierarchicalKind } from '../../../../base/common/hierarchicalKind.js';
import { Disposable } from '../../../../base/common/lifecycle.js';
import { revive } from '../../../../base/common/marshalling.js';
import { Mimes } from '../../../../base/common/mime.js';
import { Schemas } from '../../../../base/common/network.js';
import { basename, joinPath } from '../../../../base/common/resources.js';
import { URI, UriComponents } from '../../../../base/common/uri.js';
import { IRange } from '../../../../editor/common/core/range.js';
@@ -22,10 +23,9 @@ import { IFileService } from '../../../../platform/files/common/files.js';
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
import { ILogService } from '../../../../platform/log/common/log.js';
import { IExtensionService, isProposedApiEnabled } from '../../../services/extensions/common/extensions.js';
import { IChatRequestPasteVariableEntry, IChatRequestVariableEntry } from '../common/chatModel.js';
import { IChatRequestPasteVariableEntry, IChatRequestVariableEntry } from '../common/chatVariableEntries.js';
import { IChatVariablesService, IDynamicVariable } from '../common/chatVariables.js';
import { IChatWidgetService } from './chat.js';
import { ChatInputPart } from './chatInputPart.js';
import { ChatDynamicVariableModel } from './contrib/chatDynamicVariables.js';
import { cleanupOldImages, createFileForMedia, resizeImage } from './imageUtils.js';
@@ -180,7 +180,7 @@ export class CopyTextProvider implements DocumentPasteEditProvider {
public readonly pasteMimeTypes = [];
async prepareDocumentPaste(model: ITextModel, ranges: readonly IRange[], dataTransfer: IReadonlyVSDataTransfer, token: CancellationToken): Promise<undefined | IReadonlyVSDataTransfer> {
if (model.uri.scheme === ChatInputPart.INPUT_SCHEME) {
if (model.uri.scheme === Schemas.vscodeChatInput) {
return;
}
@@ -304,7 +304,7 @@ export class PasteTextProvider implements DocumentPasteEditProvider {
) { }
async provideDocumentPasteEdits(model: ITextModel, ranges: readonly IRange[], dataTransfer: IReadonlyVSDataTransfer, _context: DocumentPasteContext, token: CancellationToken): Promise<DocumentPasteEditsSession | undefined> {
if (model.uri.scheme !== ChatInputPart.INPUT_SCHEME) {
if (model.uri.scheme !== Schemas.vscodeChatInput) {
return;
}
const text = dataTransfer.get(Mimes.text);
@@ -442,9 +442,10 @@ export class ChatPasteProvidersFeature extends Disposable {
@ILogService logService: ILogService,
) {
super();
this._register(languageFeaturesService.documentPasteEditProvider.register({ scheme: ChatInputPart.INPUT_SCHEME, pattern: '*', hasAccessToAllModels: true }, instaService.createInstance(CopyAttachmentsProvider)));
this._register(languageFeaturesService.documentPasteEditProvider.register({ scheme: ChatInputPart.INPUT_SCHEME, pattern: '*', hasAccessToAllModels: true }, new PasteImageProvider(chatWidgetService, extensionService, fileService, environmentService, logService)));
this._register(languageFeaturesService.documentPasteEditProvider.register({ scheme: ChatInputPart.INPUT_SCHEME, pattern: '*', hasAccessToAllModels: true }, new PasteTextProvider(chatWidgetService, modelService)));
this._register(languageFeaturesService.documentPasteEditProvider.register({ scheme: Schemas.vscodeChatInput, pattern: '*', hasAccessToAllModels: true }, instaService.createInstance(CopyAttachmentsProvider)));
this._register(languageFeaturesService.documentPasteEditProvider.register({ scheme: Schemas.vscodeChatInput, pattern: '*', hasAccessToAllModels: true }, new PasteImageProvider(chatWidgetService, extensionService, fileService, environmentService, logService)));
this._register(languageFeaturesService.documentPasteEditProvider.register({ scheme: Schemas.vscodeChatInput, pattern: '*', hasAccessToAllModels: true }, new PasteTextProvider(chatWidgetService, modelService)));
this._register(languageFeaturesService.documentPasteEditProvider.register('*', new CopyTextProvider()));
this._register(languageFeaturesService.documentPasteEditProvider.register('*', new CopyTextProvider()));
}
}
@@ -58,7 +58,8 @@ import { IExtensionsWorkbenchService } from '../../extensions/common/extensions.
import { IChatAgentImplementation, IChatAgentRequest, IChatAgentResult, IChatAgentService } from '../common/chatAgents.js';
import { ChatContextKeys } from '../common/chatContextKeys.js';
import { ChatEntitlement, ChatEntitlementContext, ChatEntitlementRequests, ChatEntitlementService, IChatEntitlementService, isProUser } from '../common/chatEntitlementService.js';
import { ChatModel, ChatRequestModel, IChatRequestModel, IChatRequestToolEntry, IChatRequestVariableData } from '../common/chatModel.js';
import { ChatModel, ChatRequestModel, IChatRequestModel, IChatRequestVariableData } from '../common/chatModel.js';
import { IChatRequestToolEntry } from '../common/chatVariableEntries.js';
import { ChatRequestAgentPart, ChatRequestToolPart } from '../common/chatParserTypes.js';
import { IChatProgress, IChatService } from '../common/chatService.js';
import { ChatAgentLocation, ChatConfiguration, ChatMode, validateChatMode } from '../common/constants.js';
@@ -43,7 +43,7 @@ import { checkModeOption } from '../common/chat.js';
import { IChatAgentCommand, IChatAgentData, IChatAgentService, IChatWelcomeMessageContent } from '../common/chatAgents.js';
import { ChatContextKeys } from '../common/chatContextKeys.js';
import { applyingChatEditsFailedContextKey, decidedChatEditingResourceContextKey, hasAppliedChatEditsContextKey, hasUndecidedChatEditingResourceContextKey, IChatEditingService, IChatEditingSession, inChatEditingSessionContextKey, ModifiedFileEntryState } from '../common/chatEditingService.js';
import { ChatPauseState, IChatModel, IChatRequestVariableEntry, IChatResponseModel } from '../common/chatModel.js';
import { ChatPauseState, IChatModel, IChatResponseModel } from '../common/chatModel.js';
import { chatAgentLeader, ChatRequestAgentPart, ChatRequestDynamicVariablePart, ChatRequestSlashPromptPart, ChatRequestToolPart, ChatRequestToolSetPart, chatSubcommandLeader, formatChatQuestion, IParsedChatRequest } from '../common/chatParserTypes.js';
import { ChatRequestParser } from '../common/chatRequestParser.js';
import { IChatLocationData, IChatSendRequestOptions, IChatService } from '../common/chatService.js';
@@ -68,6 +68,7 @@ import './media/chatAgentHover.css';
import './media/chatViewWelcome.css';
import { ChatViewWelcomePart } from './viewsWelcome/chatViewWelcomeController.js';
import { MicrotaskDelay } from '../../../../base/common/symbols.js';
import { IChatRequestVariableEntry } from '../common/chatVariableEntries.js';
const $ = dom.$;
@@ -15,10 +15,10 @@ import { ICommandService } from '../../../../../platform/commands/common/command
import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
import { IInstantiationService, ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js';
import { ILabelService } from '../../../../../platform/label/common/label.js';
import { PromptsConfig } from '../../common/promptSyntax/config/config.js';
import { IChatRequestVariableValue, IDynamicVariable } from '../../common/chatVariables.js';
import { PromptsConfig } from '../../common/promptSyntax/config/config.js';
import { IChatWidget } from '../chat.js';
import { ChatWidget, IChatWidgetContrib } from '../chatWidget.js';
import { IChatWidgetContrib } from '../chatWidget.js';
import { ChatFileReference } from './chatDynamicVariables/chatFileReference.js';
export const dynamicVariableDecorationType = 'chat-dynamic-variable';
@@ -221,9 +221,6 @@ function isDynamicVariable(obj: any): obj is IDynamicVariable {
'data' in obj;
}
ChatWidget.CONTRIBS.push(ChatDynamicVariableModel);
export interface IAddDynamicVariableContext {
@@ -21,7 +21,7 @@ import { EditorsOrder } from '../../../../common/editor.js';
import { IEditorService } from '../../../../services/editor/common/editorService.js';
import { getNotebookEditorFromEditorPane, INotebookEditor } from '../../../notebook/browser/notebookBrowser.js';
import { IChatEditingService } from '../../common/chatEditingService.js';
import { IChatRequestFileEntry, IChatRequestImplicitVariableEntry } from '../../common/chatModel.js';
import { IChatRequestFileEntry, IChatRequestImplicitVariableEntry } from '../../common/chatVariableEntries.js';
import { IChatService } from '../../common/chatService.js';
import { ChatAgentLocation } from '../../common/constants.js';
import { ILanguageModelIgnoredFilesService } from '../../common/ignoredFiles.js';
@@ -46,16 +46,16 @@ import { IMcpPrompt, IMcpPromptMessage, IMcpServer, IMcpService, McpResourceURI
import { searchFilesAndFolders } from '../../../search/browser/chatContributions.js';
import { IChatAgentData, IChatAgentNameService, IChatAgentService, getFullyQualifiedId } from '../../common/chatAgents.js';
import { IChatEditingService } from '../../common/chatEditingService.js';
import { getAttachableImageExtension, IChatRequestVariableEntry } from '../../common/chatModel.js';
import { getAttachableImageExtension } from '../../common/chatModel.js';
import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestSlashPromptPart, ChatRequestTextPart, ChatRequestToolPart, ChatRequestToolSetPart, chatAgentLeader, chatSubcommandLeader, chatVariableLeader } from '../../common/chatParserTypes.js';
import { IChatSlashCommandService } from '../../common/chatSlashCommands.js';
import { IChatRequestVariableEntry } from '../../common/chatVariableEntries.js';
import { IDynamicVariable } from '../../common/chatVariables.js';
import { ChatAgentLocation, ChatMode } from '../../common/constants.js';
import { ToolSet } from '../../common/languageModelToolsService.js';
import { IPromptsService } from '../../common/promptSyntax/service/promptsService.js';
import { ChatSubmitAction } from '../actions/chatExecuteActions.js';
import { IChatWidget, IChatWidgetService } from '../chat.js';
import { ChatInputPart } from '../chatInputPart.js';
import { ChatDynamicVariableModel } from './chatDynamicVariables.js';
class SlashCommandCompletions extends Disposable {
@@ -68,7 +68,7 @@ class SlashCommandCompletions extends Disposable {
) {
super();
this._register(this.languageFeaturesService.completionProvider.register({ scheme: ChatInputPart.INPUT_SCHEME, hasAccessToAllModels: true }, {
this._register(this.languageFeaturesService.completionProvider.register({ scheme: Schemas.vscodeChatInput, hasAccessToAllModels: true }, {
_debugDisplayName: 'globalSlashCommands',
triggerCharacters: ['/'],
provideCompletionItems: async (model: ITextModel, position: Position, _context: CompletionContext, _token: CancellationToken) => {
@@ -115,7 +115,7 @@ class SlashCommandCompletions extends Disposable {
};
}
}));
this._register(this.languageFeaturesService.completionProvider.register({ scheme: ChatInputPart.INPUT_SCHEME, hasAccessToAllModels: true }, {
this._register(this.languageFeaturesService.completionProvider.register({ scheme: Schemas.vscodeChatInput, hasAccessToAllModels: true }, {
_debugDisplayName: 'globalSlashCommandsAt',
triggerCharacters: [chatAgentLeader],
provideCompletionItems: async (model: ITextModel, position: Position, _context: CompletionContext, _token: CancellationToken) => {
@@ -156,7 +156,7 @@ class SlashCommandCompletions extends Disposable {
};
}
}));
this._register(this.languageFeaturesService.completionProvider.register({ scheme: ChatInputPart.INPUT_SCHEME, hasAccessToAllModels: true }, {
this._register(this.languageFeaturesService.completionProvider.register({ scheme: Schemas.vscodeChatInput, hasAccessToAllModels: true }, {
_debugDisplayName: 'promptSlashCommands',
triggerCharacters: ['/'],
provideCompletionItems: async (model: ITextModel, position: Position, _context: CompletionContext, _token: CancellationToken) => {
@@ -204,7 +204,7 @@ class SlashCommandCompletions extends Disposable {
}
}));
this._register(this.languageFeaturesService.completionProvider.register({ scheme: ChatInputPart.INPUT_SCHEME, hasAccessToAllModels: true }, {
this._register(this.languageFeaturesService.completionProvider.register({ scheme: Schemas.vscodeChatInput, hasAccessToAllModels: true }, {
_debugDisplayName: 'mcpPromptSlashCommands',
triggerCharacters: ['/'],
provideCompletionItems: async (model: ITextModel, position: Position, _context: CompletionContext, _token: CancellationToken) => {
@@ -306,9 +306,9 @@ class AgentCompletions extends Disposable {
};
}
};
this._register(this.languageFeaturesService.completionProvider.register({ scheme: ChatInputPart.INPUT_SCHEME, hasAccessToAllModels: true }, subCommandProvider));
this._register(this.languageFeaturesService.completionProvider.register({ scheme: Schemas.vscodeChatInput, hasAccessToAllModels: true }, subCommandProvider));
this._register(this.languageFeaturesService.completionProvider.register({ scheme: ChatInputPart.INPUT_SCHEME, hasAccessToAllModels: true }, {
this._register(this.languageFeaturesService.completionProvider.register({ scheme: Schemas.vscodeChatInput, hasAccessToAllModels: true }, {
_debugDisplayName: 'chatAgentAndSubcommand',
triggerCharacters: [chatAgentLeader],
provideCompletionItems: async (model: ITextModel, position: Position, _context: CompletionContext, token: CancellationToken) => {
@@ -398,7 +398,7 @@ class AgentCompletions extends Disposable {
}
}));
this._register(this.languageFeaturesService.completionProvider.register({ scheme: ChatInputPart.INPUT_SCHEME, hasAccessToAllModels: true }, {
this._register(this.languageFeaturesService.completionProvider.register({ scheme: Schemas.vscodeChatInput, hasAccessToAllModels: true }, {
_debugDisplayName: 'chatAgentAndSubcommand',
triggerCharacters: [chatSubcommandLeader],
provideCompletionItems: async (model: ITextModel, position: Position, _context: CompletionContext, token: CancellationToken) => {
@@ -456,7 +456,7 @@ class AgentCompletions extends Disposable {
}
}));
this._register(this.languageFeaturesService.completionProvider.register({ scheme: ChatInputPart.INPUT_SCHEME, hasAccessToAllModels: true }, {
this._register(this.languageFeaturesService.completionProvider.register({ scheme: Schemas.vscodeChatInput, hasAccessToAllModels: true }, {
_debugDisplayName: 'installChatExtensions',
triggerCharacters: [chatAgentLeader],
provideCompletionItems: async (model: ITextModel, position: Position, _context: CompletionContext, token: CancellationToken) => {
@@ -841,7 +841,7 @@ class BuiltinDynamicCompletions extends Disposable {
}
private registerVariableCompletions(debugName: string, provider: (details: IVariableCompletionsDetails, token: CancellationToken) => ProviderResult<CompletionList>, wordPattern: RegExp = BuiltinDynamicCompletions.VariableNameDef) {
this._register(this.languageFeaturesService.completionProvider.register({ scheme: ChatInputPart.INPUT_SCHEME, hasAccessToAllModels: true }, {
this._register(this.languageFeaturesService.completionProvider.register({ scheme: Schemas.vscodeChatInput, hasAccessToAllModels: true }, {
_debugDisplayName: `chatVarCompletions-${debugName}`,
triggerCharacters: [chatVariableLeader],
provideCompletionItems: async (model: ITextModel, position: Position, context: CompletionContext, token: CancellationToken) => {
@@ -1094,7 +1094,7 @@ class ToolCompletions extends Disposable {
) {
super();
this._register(this.languageFeaturesService.completionProvider.register({ scheme: ChatInputPart.INPUT_SCHEME, hasAccessToAllModels: true }, {
this._register(this.languageFeaturesService.completionProvider.register({ scheme: Schemas.vscodeChatInput, hasAccessToAllModels: true }, {
_debugDisplayName: 'chatVariables',
triggerCharacters: [chatVariableLeader],
provideCompletionItems: async (model: ITextModel, position: Position, _context: CompletionContext, _token: CancellationToken) => {
@@ -5,7 +5,7 @@
import { VSBuffer } from '../../../../../base/common/buffer.js';
import { localize } from '../../../../../nls.js';
import { IChatRequestVariableEntry } from '../../common/chatModel.js';
import { IChatRequestVariableEntry } from '../../common/chatVariableEntries.js';
export const ScreenshotVariableId = 'screenshot-focused-window';
@@ -24,7 +24,7 @@ import { INSTRUCTIONS_LANGUAGE_ID, PromptsType } from '../../common/promptSyntax
import { compare } from '../../../../../base/common/strings.js';
import { ILabelService } from '../../../../../platform/label/common/label.js';
import { dirname } from '../../../../../base/common/resources.js';
import { IPromptFileVariableEntry } from '../../common/chatModel.js';
import { IPromptFileVariableEntry } from '../../common/chatVariableEntries.js';
import { KeyMod, KeyCode } from '../../../../../base/common/keyCodes.js';
import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js';
import { ICodeEditorService } from '../../../../../editor/browser/services/codeEditorService.js';
@@ -8,12 +8,15 @@ import { Event } from '../../../../base/common/event.js';
import { IDisposable } from '../../../../base/common/lifecycle.js';
import { IObservable, IReader } from '../../../../base/common/observable.js';
import { URI } from '../../../../base/common/uri.js';
import { StringEdit } from '../../../../editor/common/core/edits/stringEdit.js';
import { TextEdit } from '../../../../editor/common/languages.js';
import { ITextModel } from '../../../../editor/common/model.js';
import { localize } from '../../../../nls.js';
import { RawContextKey } from '../../../../platform/contextkey/common/contextkey.js';
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
import { IEditorPane } from '../../../common/editor.js';
import { ICellEditOperation } from '../../notebook/common/notebookCommon.js';
import { IChatAgentResult } from './chatAgents.js';
import { ChatModel, IChatResponseModel } from './chatModel.js';
export const IChatEditingService = createDecorator<IChatEditingService>('chatEditingService');
@@ -79,6 +82,25 @@ export interface IStreamingEdits {
export const chatEditingSnapshotScheme = 'chat-editing-snapshot-text-model';
export interface IModifiedEntryTelemetryInfo {
readonly agentId: string | undefined;
readonly command: string | undefined;
readonly sessionId: string;
readonly requestId: string;
readonly result: IChatAgentResult | undefined;
}
export interface ISnapshotEntry {
readonly resource: URI;
readonly languageId: string;
readonly snapshotUri: URI;
readonly original: string;
readonly current: string;
readonly originalToCurrentEdit: StringEdit;
readonly state: ModifiedFileEntryState;
telemetryInfo: IModifiedEntryTelemetryInfo;
}
export interface IChatEditingSession extends IDisposable {
readonly isGlobalEditingSession: boolean;
readonly chatSessionId: string;
@@ -99,6 +121,10 @@ export interface IChatEditingSession extends IDisposable {
*/
getSnapshotUri(requestId: string, uri: URI, stopId: string | undefined): URI | undefined;
getSnapshotModel(requestId: string, undoStop: string | undefined, snapshotUri: URI): Promise<ITextModel | null>;
getSnapshot(requestId: string, undoStop: string | undefined, snapshotUri: URI): ISnapshotEntry | undefined;
/**
* Will lead to this object getting disposed
*/
@@ -4,7 +4,6 @@
*--------------------------------------------------------------------------------------------*/
import { asArray } from '../../../../base/common/arrays.js';
import { Codicon } from '../../../../base/common/codicons.js';
import { Emitter, Event } from '../../../../base/common/event.js';
import { IMarkdownString, MarkdownString, isMarkdownString } from '../../../../base/common/htmlContent.js';
import { Disposable, IDisposable } from '../../../../base/common/lifecycle.js';
@@ -18,265 +17,18 @@ import { ThemeIcon } from '../../../../base/common/themables.js';
import { URI, UriComponents, UriDto, isUriComponents } from '../../../../base/common/uri.js';
import { generateUuid } from '../../../../base/common/uuid.js';
import { IRange } from '../../../../editor/common/core/range.js';
import { IOffsetRange, OffsetRange } from '../../../../editor/common/core/ranges/offsetRange.js';
import { Location, SymbolKind, TextEdit, isLocation } from '../../../../editor/common/languages.js';
import { OffsetRange } from '../../../../editor/common/core/ranges/offsetRange.js';
import { TextEdit } from '../../../../editor/common/languages.js';
import { localize } from '../../../../nls.js';
import { ILogService } from '../../../../platform/log/common/log.js';
import { IMarker, MarkerSeverity } from '../../../../platform/markers/common/markers.js';
import { CellUri, ICellEditOperation } from '../../notebook/common/notebookCommon.js';
import { ISCMHistoryItem } from '../../scm/common/history.js';
import { IChatAgentCommand, IChatAgentData, IChatAgentResult, IChatAgentService, reviveSerializedAgent } from './chatAgents.js';
import { IChatEditingService, IChatEditingSession } from './chatEditingService.js';
import { ChatRequestTextPart, IParsedChatRequest, reviveParsedChatRequest } from './chatParserTypes.js';
import { ChatAgentVoteDirection, ChatAgentVoteDownReason, IChatAgentMarkdownContentWithVulnerability, IChatCodeCitation, IChatCommandButton, IChatConfirmation, IChatContentInlineReference, IChatContentReference, IChatEditingSessionAction, IChatExtensionsContent, IChatFollowup, IChatLocationData, IChatMarkdownContent, IChatNotebookEdit, IChatPrepareToolInvocationPart, IChatProgress, IChatProgressMessage, IChatResponseCodeblockUriPart, IChatResponseProgressFileTreeData, IChatTask, IChatTaskSerialized, IChatTextEdit, IChatToolInvocation, IChatToolInvocationSerialized, IChatTreeData, IChatUndoStop, IChatUsedContext, IChatWarningMessage, isIUsedContext } from './chatService.js';
import { IChatRequestVariableValue } from './chatVariables.js';
import { IChatRequestVariableEntry } from './chatVariableEntries.js';
import { ChatAgentLocation, ChatMode } from './constants.js';
interface IBaseChatRequestVariableEntry {
readonly id: string;
readonly fullName?: string;
readonly icon?: ThemeIcon;
readonly name: string;
readonly modelDescription?: string;
/**
* The offset-range in the prompt. This means this entry has been explicitly typed out
* by the user.
*/
readonly range?: IOffsetRange;
readonly value: IChatRequestVariableValue;
readonly references?: IChatContentReference[];
omittedState?: OmittedState;
}
export interface IGenericChatRequestVariableEntry extends IBaseChatRequestVariableEntry {
kind: 'generic';
}
export interface IChatRequestDirectoryEntry extends IBaseChatRequestVariableEntry {
kind: 'directory';
}
export interface IChatRequestFileEntry extends IBaseChatRequestVariableEntry {
kind: 'file';
}
export const enum OmittedState {
NotOmitted,
Partial,
Full,
}
export interface IChatRequestToolEntry extends IBaseChatRequestVariableEntry {
readonly kind: 'tool';
}
export interface IChatRequestToolSetEntry extends IBaseChatRequestVariableEntry {
readonly kind: 'toolset';
readonly value: IChatRequestToolEntry[];
}
export interface IChatRequestImplicitVariableEntry extends IBaseChatRequestVariableEntry {
readonly kind: 'implicit';
readonly isFile: true;
readonly value: URI | Location | undefined;
readonly isSelection: boolean;
readonly isPromptFile: boolean;
readonly enabled: boolean;
}
export interface IChatRequestPasteVariableEntry extends IBaseChatRequestVariableEntry {
readonly kind: 'paste';
readonly code: string;
readonly language: string;
readonly pastedLines: string;
// This is only used for old serialized data and should be removed once we no longer support it
readonly fileName: string;
// This is only undefined on old serialized data
readonly copiedFrom: {
readonly uri: URI;
readonly range: IRange;
} | undefined;
}
export interface ISymbolVariableEntry extends IBaseChatRequestVariableEntry {
readonly kind: 'symbol';
readonly value: Location;
readonly symbolKind: SymbolKind;
}
export interface ICommandResultVariableEntry extends IBaseChatRequestVariableEntry {
readonly kind: 'command';
}
export interface IImageVariableEntry extends IBaseChatRequestVariableEntry {
readonly kind: 'image';
readonly isPasted?: boolean;
readonly isURL?: boolean;
readonly mimeType?: string;
}
export interface INotebookOutputVariableEntry extends IBaseChatRequestVariableEntry {
readonly kind: 'notebookOutput';
readonly outputIndex?: number;
readonly mimeType?: string;
}
export interface IDiagnosticVariableEntryFilterData {
readonly owner?: string;
readonly problemMessage?: string;
readonly filterUri?: URI;
readonly filterSeverity?: MarkerSeverity;
readonly filterRange?: IRange;
}
/**
* Chat variable that represents an attached prompt file.
*/
export interface IPromptVariableEntry extends IBaseChatRequestVariableEntry {
readonly kind: 'file';
readonly value: URI | Location;
readonly isRoot: boolean;
readonly modelDescription: string;
}
export namespace IDiagnosticVariableEntryFilterData {
export const icon = Codicon.error;
export function fromMarker(marker: IMarker): IDiagnosticVariableEntryFilterData {
return {
filterUri: marker.resource,
owner: marker.owner,
problemMessage: marker.message,
filterRange: { startLineNumber: marker.startLineNumber, endLineNumber: marker.endLineNumber, startColumn: marker.startColumn, endColumn: marker.endColumn }
};
}
export function toEntry(data: IDiagnosticVariableEntryFilterData): IDiagnosticVariableEntry {
return {
id: id(data),
name: label(data),
icon,
value: data,
kind: 'diagnostic',
...data,
};
}
export function id(data: IDiagnosticVariableEntryFilterData) {
return [data.filterUri, data.owner, data.filterSeverity, data.filterRange?.startLineNumber].join(':');
}
export function label(data: IDiagnosticVariableEntryFilterData) {
const enum TrimThreshold {
MaxChars = 30,
MaxSpaceLookback = 10,
}
if (data.problemMessage) {
if (data.problemMessage.length < TrimThreshold.MaxChars) {
return data.problemMessage;
}
// Trim the message, on a space if it would not lose too much
// data (MaxSpaceLookback) or just blindly otherwise.
const lastSpace = data.problemMessage.lastIndexOf(' ', TrimThreshold.MaxChars);
if (lastSpace === -1 || lastSpace + TrimThreshold.MaxSpaceLookback < TrimThreshold.MaxChars) {
return data.problemMessage.substring(0, TrimThreshold.MaxChars) + '…';
}
return data.problemMessage.substring(0, lastSpace) + '…';
}
let labelStr = localize('chat.attachment.problems.all', "All Problems");
if (data.filterUri) {
labelStr = localize('chat.attachment.problems.inFile', "Problems in {0}", basename(data.filterUri));
}
return labelStr;
}
}
export interface IDiagnosticVariableEntry extends IBaseChatRequestVariableEntry, IDiagnosticVariableEntryFilterData {
readonly kind: 'diagnostic';
}
export interface IElementVariableEntry extends IBaseChatRequestVariableEntry {
readonly kind: 'element';
}
export interface IPromptFileVariableEntry extends IBaseChatRequestVariableEntry {
readonly kind: 'promptFile';
}
export interface ISCMHistoryItemVariableEntry extends IBaseChatRequestVariableEntry {
readonly kind: 'scmHistoryItem';
readonly value: URI;
readonly historyItem: ISCMHistoryItem;
}
export type IChatRequestVariableEntry = IGenericChatRequestVariableEntry | IChatRequestImplicitVariableEntry | IChatRequestPasteVariableEntry
| ISymbolVariableEntry | ICommandResultVariableEntry | IDiagnosticVariableEntry | IImageVariableEntry
| IChatRequestToolEntry | IChatRequestToolSetEntry
| IChatRequestDirectoryEntry | IChatRequestFileEntry | INotebookOutputVariableEntry | IElementVariableEntry
| IPromptFileVariableEntry | ISCMHistoryItemVariableEntry;
export namespace IChatRequestVariableEntry {
/**
* Returns URI of the passed variant entry. Return undefined if not found.
*/
export function toUri(entry: IChatRequestVariableEntry): URI | undefined {
return URI.isUri(entry.value)
? entry.value
: isLocation(entry.value)
? entry.value.uri
: undefined;
}
}
export function isImplicitVariableEntry(obj: IChatRequestVariableEntry): obj is IChatRequestImplicitVariableEntry {
return obj.kind === 'implicit';
}
export function isPasteVariableEntry(obj: IChatRequestVariableEntry): obj is IChatRequestPasteVariableEntry {
return obj.kind === 'paste';
}
export function isImageVariableEntry(obj: IChatRequestVariableEntry): obj is IImageVariableEntry {
return obj.kind === 'image';
}
export function isNotebookOutputVariableEntry(obj: IChatRequestVariableEntry): obj is INotebookOutputVariableEntry {
return obj.kind === 'notebookOutput';
}
export function isElementVariableEntry(obj: IChatRequestVariableEntry): obj is IElementVariableEntry {
return obj.kind === 'element';
}
export function isDiagnosticsVariableEntry(obj: IChatRequestVariableEntry): obj is IDiagnosticVariableEntry {
return obj.kind === 'diagnostic';
}
export function isChatRequestFileEntry(obj: IChatRequestVariableEntry): obj is IChatRequestFileEntry {
return obj.kind === 'file';
}
export function isChatRequestVariableEntry(obj: unknown): obj is IChatRequestVariableEntry {
const entry = obj as IChatRequestVariableEntry;
return typeof entry === 'object' &&
entry !== null &&
typeof entry.id === 'string' &&
typeof entry.name === 'string';
}
export function isSCMHistoryItemVariableEntry(obj: IChatRequestVariableEntry): obj is ISCMHistoryItemVariableEntry {
return obj.kind === 'scmHistoryItem';
}
export const CHAT_ATTACHABLE_IMAGE_MIME_TYPES: Record<string, string> = {
png: 'image/png',
@@ -8,12 +8,12 @@ import { ThemeIcon } from '../../../../base/common/themables.js';
import { IOffsetRange, OffsetRange } from '../../../../editor/common/core/ranges/offsetRange.js';
import { IRange } from '../../../../editor/common/core/range.js';
import { IChatAgentCommand, IChatAgentData, IChatAgentService, reviveSerializedAgent } from './chatAgents.js';
import { IChatRequestToolEntry, IChatRequestToolSetEntry, IChatRequestVariableEntry, IDiagnosticVariableEntryFilterData } from './chatModel.js';
import { IChatSlashData } from './chatSlashCommands.js';
import { IChatRequestProblemsVariable, IChatRequestVariableValue } from './chatVariables.js';
import { ChatAgentLocation } from './constants.js';
import { IToolData } from './languageModelToolsService.js';
import { IChatPromptSlashCommand } from './promptSyntax/service/promptsService.js';
import { IChatRequestToolEntry, IChatRequestToolSetEntry, IChatRequestVariableEntry, IDiagnosticVariableEntryFilterData } from './chatVariableEntries.js';
// These are in a separate file to avoid circular dependencies with the dependencies of the parser
@@ -18,9 +18,10 @@ import { createDecorator } from '../../../../platform/instantiation/common/insta
import { ICellEditOperation } from '../../notebook/common/notebookCommon.js';
import { IWorkspaceSymbol } from '../../search/common/search.js';
import { IChatAgentCommand, IChatAgentData, IChatAgentResult } from './chatAgents.js';
import { ChatModel, IChatModel, IChatRequestModel, IChatRequestVariableData, IChatRequestVariableEntry, IChatResponseModel, IExportableChatData, ISerializableChatData } from './chatModel.js';
import { ChatModel, IChatModel, IChatRequestModel, IChatRequestVariableData, IChatResponseModel, IExportableChatData, ISerializableChatData } from './chatModel.js';
import { IParsedChatRequest } from './chatParserTypes.js';
import { IChatParserContext } from './chatRequestParser.js';
import { IChatRequestVariableEntry } from './chatVariableEntries.js';
import { IChatRequestVariableValue } from './chatVariables.js';
import { ChatAgentLocation, ChatMode } from './constants.js';
import { IPreparedToolInvocation, IToolConfirmationMessages, IToolResult } from './languageModelToolsService.js';
@@ -27,7 +27,7 @@ import { IWorkspaceContextService } from '../../../../platform/workspace/common/
import { IWorkbenchAssignmentService } from '../../../services/assignment/common/assignmentService.js';
import { IExtensionService } from '../../../services/extensions/common/extensions.js';
import { IChatAgent, IChatAgentCommand, IChatAgentData, IChatAgentHistoryEntry, IChatAgentRequest, IChatAgentResult, IChatAgentService } from './chatAgents.js';
import { ChatModel, ChatRequestModel, ChatRequestRemovalReason, IChatModel, IChatRequestModel, IChatRequestVariableData, IChatRequestVariableEntry, IChatResponseModel, IExportableChatData, ISerializableChatData, ISerializableChatDataIn, ISerializableChatsData, isImageVariableEntry, normalizeSerializableChatData, toChatHistoryContent, updateRanges } from './chatModel.js';
import { ChatModel, ChatRequestModel, ChatRequestRemovalReason, IChatModel, IChatRequestModel, IChatRequestVariableData, IChatResponseModel, IExportableChatData, ISerializableChatData, ISerializableChatDataIn, ISerializableChatsData, normalizeSerializableChatData, toChatHistoryContent, updateRanges } from './chatModel.js';
import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestSlashCommandPart, IParsedChatRequest, chatAgentLeader, chatSubcommandLeader, getPromptText } from './chatParserTypes.js';
import { ChatRequestParser } from './chatRequestParser.js';
import { IChatCompleteResponse, IChatDetail, IChatFollowup, IChatProgress, IChatSendRequestData, IChatSendRequestOptions, IChatSendRequestResponseState, IChatService, IChatTransferredSessionData, IChatUserActionEvent } from './chatService.js';
@@ -35,6 +35,7 @@ import { ChatServiceTelemetry } from './chatServiceTelemetry.js';
import { ChatSessionStore, IChatTransfer2 } from './chatSessionStore.js';
import { IChatSlashCommandService } from './chatSlashCommands.js';
import { IChatTransferService } from './chatTransferService.js';
import { IChatRequestVariableEntry, isImageVariableEntry } from './chatVariableEntries.js';
import { ChatAgentLocation, ChatConfiguration, ChatMode } from './constants.js';
import { ChatMessageRole, IChatMessage, ILanguageModelsService } from './languageModels.js';
import { ILanguageModelToolsService } from './languageModelToolsService.js';
@@ -0,0 +1,263 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Codicon } from '../../../../base/common/codicons.js';
import { basename } from '../../../../base/common/resources.js';
import { ThemeIcon } from '../../../../base/common/themables.js';
import { URI } from '../../../../base/common/uri.js';
import { IRange } from '../../../../editor/common/core/range.js';
import { IOffsetRange } from '../../../../editor/common/core/ranges/offsetRange.js';
import { isLocation, Location, SymbolKind } from '../../../../editor/common/languages.js';
import { localize } from '../../../../nls.js';
import { MarkerSeverity, IMarker } from '../../../../platform/markers/common/markers.js';
import { ISCMHistoryItem } from '../../scm/common/history.js';
import { IChatContentReference } from './chatService.js';
import { IChatRequestVariableValue } from './chatVariables.js';
interface IBaseChatRequestVariableEntry {
readonly id: string;
readonly fullName?: string;
readonly icon?: ThemeIcon;
readonly name: string;
readonly modelDescription?: string;
/**
* The offset-range in the prompt. This means this entry has been explicitly typed out
* by the user.
*/
readonly range?: IOffsetRange;
readonly value: IChatRequestVariableValue;
readonly references?: IChatContentReference[];
omittedState?: OmittedState;
}
export interface IGenericChatRequestVariableEntry extends IBaseChatRequestVariableEntry {
kind: 'generic';
}
export interface IChatRequestDirectoryEntry extends IBaseChatRequestVariableEntry {
kind: 'directory';
}
export interface IChatRequestFileEntry extends IBaseChatRequestVariableEntry {
kind: 'file';
}
export const enum OmittedState {
NotOmitted,
Partial,
Full,
}
export interface IChatRequestToolEntry extends IBaseChatRequestVariableEntry {
readonly kind: 'tool';
}
export interface IChatRequestToolSetEntry extends IBaseChatRequestVariableEntry {
readonly kind: 'toolset';
readonly value: IChatRequestToolEntry[];
}
export interface IChatRequestImplicitVariableEntry extends IBaseChatRequestVariableEntry {
readonly kind: 'implicit';
readonly isFile: true;
readonly value: URI | Location | undefined;
readonly isSelection: boolean;
readonly isPromptFile: boolean;
readonly enabled: boolean;
}
export interface IChatRequestPasteVariableEntry extends IBaseChatRequestVariableEntry {
readonly kind: 'paste';
readonly code: string;
readonly language: string;
readonly pastedLines: string;
// This is only used for old serialized data and should be removed once we no longer support it
readonly fileName: string;
// This is only undefined on old serialized data
readonly copiedFrom: {
readonly uri: URI;
readonly range: IRange;
} | undefined;
}
export interface ISymbolVariableEntry extends IBaseChatRequestVariableEntry {
readonly kind: 'symbol';
readonly value: Location;
readonly symbolKind: SymbolKind;
}
export interface ICommandResultVariableEntry extends IBaseChatRequestVariableEntry {
readonly kind: 'command';
}
export interface IImageVariableEntry extends IBaseChatRequestVariableEntry {
readonly kind: 'image';
readonly isPasted?: boolean;
readonly isURL?: boolean;
readonly mimeType?: string;
}
export interface INotebookOutputVariableEntry extends IBaseChatRequestVariableEntry {
readonly kind: 'notebookOutput';
readonly outputIndex?: number;
readonly mimeType?: string;
}
export interface IDiagnosticVariableEntryFilterData {
readonly owner?: string;
readonly problemMessage?: string;
readonly filterUri?: URI;
readonly filterSeverity?: MarkerSeverity;
readonly filterRange?: IRange;
}
/**
* Chat variable that represents an attached prompt file.
*/
export interface IPromptVariableEntry extends IBaseChatRequestVariableEntry {
readonly kind: 'file';
readonly value: URI | Location;
readonly isRoot: boolean;
readonly modelDescription: string;
}
export namespace IDiagnosticVariableEntryFilterData {
export const icon = Codicon.error;
export function fromMarker(marker: IMarker): IDiagnosticVariableEntryFilterData {
return {
filterUri: marker.resource,
owner: marker.owner,
problemMessage: marker.message,
filterRange: { startLineNumber: marker.startLineNumber, endLineNumber: marker.endLineNumber, startColumn: marker.startColumn, endColumn: marker.endColumn }
};
}
export function toEntry(data: IDiagnosticVariableEntryFilterData): IDiagnosticVariableEntry {
return {
id: id(data),
name: label(data),
icon,
value: data,
kind: 'diagnostic',
...data,
};
}
export function id(data: IDiagnosticVariableEntryFilterData) {
return [data.filterUri, data.owner, data.filterSeverity, data.filterRange?.startLineNumber].join(':');
}
export function label(data: IDiagnosticVariableEntryFilterData) {
const enum TrimThreshold {
MaxChars = 30,
MaxSpaceLookback = 10,
}
if (data.problemMessage) {
if (data.problemMessage.length < TrimThreshold.MaxChars) {
return data.problemMessage;
}
// Trim the message, on a space if it would not lose too much
// data (MaxSpaceLookback) or just blindly otherwise.
const lastSpace = data.problemMessage.lastIndexOf(' ', TrimThreshold.MaxChars);
if (lastSpace === -1 || lastSpace + TrimThreshold.MaxSpaceLookback < TrimThreshold.MaxChars) {
return data.problemMessage.substring(0, TrimThreshold.MaxChars) + '…';
}
return data.problemMessage.substring(0, lastSpace) + '…';
}
let labelStr = localize('chat.attachment.problems.all', "All Problems");
if (data.filterUri) {
labelStr = localize('chat.attachment.problems.inFile', "Problems in {0}", basename(data.filterUri));
}
return labelStr;
}
}
export interface IDiagnosticVariableEntry extends IBaseChatRequestVariableEntry, IDiagnosticVariableEntryFilterData {
readonly kind: 'diagnostic';
}
export interface IElementVariableEntry extends IBaseChatRequestVariableEntry {
readonly kind: 'element';
}
export interface IPromptFileVariableEntry extends IBaseChatRequestVariableEntry {
readonly kind: 'promptFile';
}
export interface ISCMHistoryItemVariableEntry extends IBaseChatRequestVariableEntry {
readonly kind: 'scmHistoryItem';
readonly value: URI;
readonly historyItem: ISCMHistoryItem;
}
export type IChatRequestVariableEntry = IGenericChatRequestVariableEntry | IChatRequestImplicitVariableEntry | IChatRequestPasteVariableEntry
| ISymbolVariableEntry | ICommandResultVariableEntry | IDiagnosticVariableEntry | IImageVariableEntry
| IChatRequestToolEntry | IChatRequestToolSetEntry
| IChatRequestDirectoryEntry | IChatRequestFileEntry | INotebookOutputVariableEntry | IElementVariableEntry
| IPromptFileVariableEntry | ISCMHistoryItemVariableEntry;
export namespace IChatRequestVariableEntry {
/**
* Returns URI of the passed variant entry. Return undefined if not found.
*/
export function toUri(entry: IChatRequestVariableEntry): URI | undefined {
return URI.isUri(entry.value)
? entry.value
: isLocation(entry.value)
? entry.value.uri
: undefined;
}
}
export function isImplicitVariableEntry(obj: IChatRequestVariableEntry): obj is IChatRequestImplicitVariableEntry {
return obj.kind === 'implicit';
}
export function isPasteVariableEntry(obj: IChatRequestVariableEntry): obj is IChatRequestPasteVariableEntry {
return obj.kind === 'paste';
}
export function isImageVariableEntry(obj: IChatRequestVariableEntry): obj is IImageVariableEntry {
return obj.kind === 'image';
}
export function isNotebookOutputVariableEntry(obj: IChatRequestVariableEntry): obj is INotebookOutputVariableEntry {
return obj.kind === 'notebookOutput';
}
export function isElementVariableEntry(obj: IChatRequestVariableEntry): obj is IElementVariableEntry {
return obj.kind === 'element';
}
export function isDiagnosticsVariableEntry(obj: IChatRequestVariableEntry): obj is IDiagnosticVariableEntry {
return obj.kind === 'diagnostic';
}
export function isChatRequestFileEntry(obj: IChatRequestVariableEntry): obj is IChatRequestFileEntry {
return obj.kind === 'file';
}
export function isChatRequestVariableEntry(obj: unknown): obj is IChatRequestVariableEntry {
const entry = obj as IChatRequestVariableEntry;
return typeof entry === 'object' &&
entry !== null &&
typeof entry.id === 'string' &&
typeof entry.name === 'string';
}
export function isSCMHistoryItemVariableEntry(obj: IChatRequestVariableEntry): obj is ISCMHistoryItemVariableEntry {
return obj.kind === 'scmHistoryItem';
}
@@ -9,8 +9,9 @@ import { URI } from '../../../../base/common/uri.js';
import { IRange } from '../../../../editor/common/core/range.js';
import { Location } from '../../../../editor/common/languages.js';
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
import { IChatModel, IDiagnosticVariableEntryFilterData } from './chatModel.js';
import { IChatModel } from './chatModel.js';
import { IChatContentReference, IChatProgressMessage } from './chatService.js';
import { IDiagnosticVariableEntryFilterData } from './chatVariableEntries.js';
import { IToolData, ToolSet } from './languageModelToolsService.js';
export interface IChatVariableData {
@@ -15,7 +15,8 @@ import { IInstantiationService } from '../../../../platform/instantiation/common
import { ILogService } from '../../../../platform/log/common/log.js';
import { annotateVulnerabilitiesInText } from './annotations.js';
import { getFullyQualifiedId, IChatAgentCommand, IChatAgentData, IChatAgentNameService, IChatAgentResult } from './chatAgents.js';
import { ChatPauseState, IChatModel, IChatProgressRenderableResponseContent, IChatRequestDisablement, IChatRequestModel, IChatRequestVariableEntry, IChatResponseModel, IChatTextEditGroup, IResponse } from './chatModel.js';
import { ChatPauseState, IChatModel, IChatProgressRenderableResponseContent, IChatRequestDisablement, IChatRequestModel, IChatResponseModel, IChatTextEditGroup, IResponse } from './chatModel.js';
import { IChatRequestVariableEntry } from './chatVariableEntries.js';
import { IParsedChatRequest } from './chatParserTypes.js';
import { ChatAgentVoteDirection, ChatAgentVoteDownReason, IChatCodeCitation, IChatContentReference, IChatFollowup, IChatProgressMessage, IChatResponseErrorDetails, IChatTask, IChatUsedContext } from './chatService.js';
import { countWords } from './chatWordCounter.js';
@@ -9,7 +9,7 @@ import { createDecorator } from '../../../../platform/instantiation/common/insta
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
import { Memento } from '../../../common/memento.js';
import { ModifiedFileEntryState } from './chatEditingService.js';
import { IChatRequestVariableEntry } from './chatModel.js';
import { IChatRequestVariableEntry } from './chatVariableEntries.js';
import { IChatMode } from './chatModes.js';
import { CHAT_PROVIDER_ID } from './chatParticipantContribTypes.js';
import { ChatAgentLocation, ChatMode } from './constants.js';
@@ -4,9 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { PromptToken } from './promptToken.js';
import { assert } from '../../../../../../../base/common/assert.js';
import { Range } from '../../../../../../../editor/common/core/range.js';
import { INVALID_NAME_CHARACTERS, STOP_CHARACTERS } from '../parsers/promptSlashCommandParser.js';
/**
* All prompt at-mentions start with `/` character.
@@ -24,14 +22,6 @@ export class PromptSlashCommand extends PromptToken {
*/
public readonly name: string,
) {
// sanity check of characters used in the provided command name
for (const character of name) {
assert(
(INVALID_NAME_CHARACTERS.includes(character) === false) &&
(STOP_CHARACTERS.includes(character) === false),
`Slash command 'name' cannot contain character '${character}', got '${name}'.`,
);
}
super(range);
}
@@ -4,9 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { PromptToken } from './promptToken.js';
import { assert } from '../../../../../../../base/common/assert.js';
import { IRange, Range } from '../../../../../../../editor/common/core/range.js';
import { INVALID_NAME_CHARACTERS, STOP_CHARACTERS } from '../parsers/promptVariableParser.js';
/**
* All prompt variables start with `#` character.
@@ -29,14 +27,6 @@ export class PromptVariable extends PromptToken {
*/
public readonly name: string,
) {
// sanity check of characters used in the provided variable name
for (const character of name) {
assert(
(INVALID_NAME_CHARACTERS.includes(character) === false) &&
(STOP_CHARACTERS.includes(character) === false),
`Variable 'name' cannot contain character '${character}', got '${name}'.`,
);
}
super(range);
}
@@ -74,14 +64,6 @@ export class PromptVariableWithData extends PromptVariable {
public readonly data: string,
) {
super(fullRange, name);
// sanity check of characters used in the provided variable data
for (const character of data) {
assert(
(STOP_CHARACTERS.includes(character) === false),
`Variable 'data' cannot contain character '${character}', got '${data}'.`,
);
}
}
/**
@@ -15,10 +15,9 @@ import { FileService } from '../../../../../platform/files/common/fileService.js
import { InMemoryFileSystemProvider } from '../../../../../platform/files/common/inMemoryFilesystemProvider.js';
import { NullLogService } from '../../../../../platform/log/common/log.js';
import { TestEnvironmentService } from '../../../../test/browser/workbenchTestServices.js';
import { ISnapshotEntry } from '../../browser/chatEditing/chatEditingModifiedFileEntry.js';
import { ChatEditingSessionStorage, IChatEditingSessionStop, StoredSessionState } from '../../browser/chatEditing/chatEditingSessionStorage.js';
import { ChatEditingSnapshotTextModelContentProvider } from '../../browser/chatEditing/chatEditingTextModelContentProviders.js';
import { ModifiedFileEntryState } from '../../common/chatEditingService.js';
import { ISnapshotEntry, ModifiedFileEntryState } from '../../common/chatEditingService.js';
suite('ChatEditingSessionStorage', () => {
const ds = ensureNoDisposablesAreLeakedInTestSuite();
@@ -43,7 +43,8 @@ import { IEditorService, SIDE_GROUP } from '../../../services/editor/common/edit
import { IViewsService } from '../../../services/views/common/viewsService.js';
import { showChatView } from '../../chat/browser/chat.js';
import { IChatWidgetLocationOptions } from '../../chat/browser/chatWidget.js';
import { ChatModel, ChatRequestRemovalReason, IChatRequestModel, IChatRequestVariableEntry, IChatTextEditGroup, IChatTextEditGroupState, IResponse } from '../../chat/common/chatModel.js';
import { ChatModel, ChatRequestRemovalReason, IChatRequestModel, IChatTextEditGroup, IChatTextEditGroupState, IResponse } from '../../chat/common/chatModel.js';
import { IChatRequestVariableEntry } from '../../chat/common/chatVariableEntries.js';
import { IChatService } from '../../chat/common/chatService.js';
import { INotebookEditorService } from '../../notebook/browser/services/notebookEditorService.js';
import { CTX_INLINE_CHAT_EDITING, CTX_INLINE_CHAT_REQUEST_IN_PROGRESS, CTX_INLINE_CHAT_RESPONSE_TYPE, CTX_INLINE_CHAT_VISIBLE, INLINE_CHAT_ID, InlineChatConfigKeys, InlineChatResponseType } from '../common/inlineChat.js';
@@ -15,7 +15,7 @@ import { IMarkerService, MarkerSeverity } from '../../../../platform/markers/com
import { IQuickPickSeparator } from '../../../../platform/quickinput/common/quickInput.js';
import { IWorkbenchContribution } from '../../../common/contributions.js';
import { IChatContextPickerItem, IChatContextPickerPickItem, IChatContextPickService } from '../../chat/browser/chatContextPickService.js';
import { IDiagnosticVariableEntryFilterData } from '../../chat/common/chatModel.js';
import { IDiagnosticVariableEntryFilterData } from '../../chat/common/chatVariableEntries.js';
class MarkerChatContextPick implements IChatContextPickerItem {
@@ -5,7 +5,6 @@
import { h } from '../../../../base/browser/dom.js';
import { assertNever } from '../../../../base/common/assert.js';
import { raceTimeout } from '../../../../base/common/async.js';
import { Codicon } from '../../../../base/common/codicons.js';
import { groupBy } from '../../../../base/common/collections.js';
import { Event } from '../../../../base/common/event.js';
@@ -34,9 +33,9 @@ import { ActiveEditorContext, ResourceContextKey } from '../../../common/context
import { IWorkbenchContribution } from '../../../common/contributions.js';
import { IEditorService } from '../../../services/editor/common/editorService.js';
import { IViewsService } from '../../../services/views/common/viewsService.js';
import { ChatViewId, IChatWidget, IChatWidgetService } from '../../chat/browser/chat.js';
import { IChatWidgetService } from '../../chat/browser/chat.js';
import { ChatContextKeys } from '../../chat/common/chatContextKeys.js';
import { ChatAgentLocation, ChatMode } from '../../chat/common/constants.js';
import { ChatMode } from '../../chat/common/constants.js';
import { ILanguageModelsService } from '../../chat/common/languageModels.js';
import { extensionsFilterSubMenu, IExtensionsWorkbenchService } from '../../extensions/common/extensions.js';
import { TEXT_FILE_EDITOR_ID } from '../../files/common/files.js';
@@ -47,6 +46,7 @@ import { IMcpSamplingService, IMcpServer, IMcpServerStartOpts, IMcpService, IMcp
import { McpAddConfigurationCommand } from './mcpCommandsAddConfiguration.js';
import { McpResourceQuickAccess, McpResourceQuickPick } from './mcpResourceQuickAccess.js';
import { McpUrlHandler } from './mcpUrlHandler.js';
import { openPanelChatAndGetWidget } from './openPanelChatAndGetWidget.js';
// acroynms do not get localized
const category: ILocalizedString = {
@@ -135,7 +135,7 @@ export class ListMcpServerCommand extends Action2 {
if (!picked) {
// no-op
} else if (picked.id === '$add') {
commandService.executeCommand(AddConfigurationAction.ID);
commandService.executeCommand(McpCommandIds.AddConfiguration);
} else {
commandService.executeCommand(McpCommandIds.ServerOptions, picked.id);
}
@@ -454,11 +454,9 @@ export class ResetMcpCachedTools extends Action2 {
}
export class AddConfigurationAction extends Action2 {
static readonly ID = 'workbench.mcp.addConfiguration';
constructor() {
super({
id: AddConfigurationAction.ID,
id: McpCommandIds.AddConfiguration,
title: localize2('mcp.addConfiguration', "Add Server..."),
metadata: {
description: localize2('mcp.addConfiguration.description', "Installs a new Model Context protocol to the mcp.json settings"),
@@ -747,18 +745,4 @@ export class McpStartPromptingServerCommand extends Action2 {
}
}
export async function openPanelChatAndGetWidget(viewsService: IViewsService, chatService: IChatWidgetService): Promise<IChatWidget | undefined> {
await viewsService.openView(ChatViewId, true);
const widgets = chatService.getWidgetsByLocations(ChatAgentLocation.Panel);
if (widgets.length) {
return widgets[0];
}
const eventPromise = Event.toPromise(Event.filter(chatService.onDidAddWidget, e => e.location === ChatAgentLocation.Panel));
return await raceTimeout(
eventPromise,
10_000, // should be enough time for chat to initialize...
() => eventPromise.cancel(),
);
}
@@ -23,10 +23,10 @@ import { IEditorService } from '../../../services/editor/common/editorService.js
import { IViewsService } from '../../../services/views/common/viewsService.js';
import { IChatWidgetService } from '../../chat/browser/chat.js';
import { resolveImageEditorAttachContext } from '../../chat/browser/chatAttachmentResolve.js';
import { IChatRequestVariableEntry } from '../../chat/common/chatModel.js';
import { IChatRequestVariableEntry } from '../../chat/common/chatVariableEntries.js';
import { IMcpResource, IMcpResourceTemplate, IMcpServer, IMcpService, isMcpResourceTemplate, McpCapability, McpConnectionState, McpResourceURI } from '../common/mcpTypes.js';
import { IUriTemplateVariable } from '../common/uriTemplate.js';
import { openPanelChatAndGetWidget } from './mcpCommands.js';
import { openPanelChatAndGetWidget } from './openPanelChatAndGetWidget.js';
export class McpResourcePickHelper {
public static sep(server: IMcpServer): IQuickPickSeparator {
@@ -0,0 +1,27 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { raceTimeout } from '../../../../base/common/async.js';
import { Event } from '../../../../base/common/event.js';
import { IViewsService } from '../../../services/views/common/viewsService.js';
import { IChatWidgetService, IChatWidget, ChatViewId } from '../../chat/browser/chat.js';
import { ChatAgentLocation } from '../../chat/common/constants.js';
export async function openPanelChatAndGetWidget(viewsService: IViewsService, chatService: IChatWidgetService): Promise<IChatWidget | undefined> {
await viewsService.openView(ChatViewId, true);
const widgets = chatService.getWidgetsByLocations(ChatAgentLocation.Panel);
if (widgets.length) {
return widgets[0];
}
const eventPromise = Event.toPromise(Event.filter(chatService.onDidAddWidget, e => e.location === ChatAgentLocation.Panel));
return await raceTimeout(
eventPromise,
10000, // should be enough time for chat to initialize...
() => eventPromise.cancel()
);
}
@@ -7,7 +7,7 @@ import { normalizeDriveLetter } from '../../../../../../base/common/labels.js';
import { basenameOrAuthority } from '../../../../../../base/common/resources.js';
import { ThemeIcon } from '../../../../../../base/common/themables.js';
import { localize } from '../../../../../../nls.js';
import { INotebookOutputVariableEntry } from '../../../../chat/common/chatModel.js';
import { INotebookOutputVariableEntry } from '../../../../chat/common/chatVariableEntries.js';
import { CellUri } from '../../../common/notebookCommon.js';
import { ICellOutputViewModel, INotebookEditor } from '../../notebookBrowser.js';
@@ -6,6 +6,7 @@
import { CancellationToken } from '../../../../../../base/common/cancellation.js';
import { codiconsLibrary } from '../../../../../../base/common/codiconsLibrary.js';
import { Disposable } from '../../../../../../base/common/lifecycle.js';
import { Schemas } from '../../../../../../base/common/network.js';
import { Position } from '../../../../../../editor/common/core/position.js';
import { Range } from '../../../../../../editor/common/core/range.js';
import { IWordAtPosition } from '../../../../../../editor/common/core/wordHelper.js';
@@ -20,7 +21,6 @@ import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from '../../.
import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../../../common/contributions.js';
import { IEditorService } from '../../../../../services/editor/common/editorService.js';
import { IChatWidget, IChatWidgetService, showChatView } from '../../../../chat/browser/chat.js';
import { ChatInputPart } from '../../../../chat/browser/chatInputPart.js';
import { ChatDynamicVariableModel } from '../../../../chat/browser/contrib/chatDynamicVariables.js';
import { computeCompletionRanges } from '../../../../chat/browser/contrib/chatInputCompletions.js';
import { IChatAgentService } from '../../../../chat/common/chatAgents.js';
@@ -70,7 +70,7 @@ class NotebookChatContribution extends Disposable implements IWorkbenchContribut
updateNotebookAgentStatus();
this._register(chatAgentService.onDidChangeAgents(updateNotebookAgentStatus));
this._register(this.languageFeaturesService.completionProvider.register({ scheme: ChatInputPart.INPUT_SCHEME, hasAccessToAllModels: true }, {
this._register(this.languageFeaturesService.completionProvider.register({ scheme: Schemas.vscodeChatInput, hasAccessToAllModels: true }, {
_debugDisplayName: 'chatKernelDynamicCompletions',
triggerCharacters: [chatVariableLeader],
provideCompletionItems: async (model: ITextModel, position: Position, _context: CompletionContext, token: CancellationToken) => {
@@ -22,7 +22,7 @@ import { IEditorPane, IEditorPaneWithSelection } from '../../../common/editor.js
import { CellViewModelStateChangeEvent, NotebookCellStateChangedEvent, NotebookLayoutInfo } from './notebookViewEvents.js';
import { NotebookCellTextModel } from '../common/model/notebookCellTextModel.js';
import { NotebookTextModel } from '../common/model/notebookTextModel.js';
import { CellKind, ICellOutput, INotebookCellStatusBarItem, INotebookRendererInfo, INotebookFindOptions, IOrderedMimeType, NotebookCellInternalMetadata, NotebookCellMetadata, NOTEBOOK_EDITOR_ID } from '../common/notebookCommon.js';
import { CellKind, ICellOutput, INotebookCellStatusBarItem, INotebookRendererInfo, INotebookFindOptions, IOrderedMimeType, NotebookCellInternalMetadata, NotebookCellMetadata, NOTEBOOK_EDITOR_ID, NOTEBOOK_DIFF_EDITOR_ID } from '../common/notebookCommon.js';
import { isCompositeNotebookEditorInput } from '../common/notebookEditorInput.js';
import { INotebookKernel } from '../common/notebookKernelService.js';
import { NotebookOptions } from './notebookOptions.js';
@@ -32,7 +32,6 @@ import { IEditorCommentsOptions, IEditorOptions } from '../../../../editor/commo
import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js';
import { IObservable } from '../../../../base/common/observable.js';
import { NotebookTextDiffEditor } from './diff/notebookDiffEditor.js';
import { INotebookTextDiffEditor } from './diff/notebookDiffEditorBrowser.js';
//#region Shared commands
@@ -931,7 +930,7 @@ export function getNotebookEditorFromEditorPane(editorPane?: IEditorPane): INote
return editorPane.getControl() as INotebookEditor | undefined;
}
if (editorPane.getId() === NotebookTextDiffEditor.ID) {
if (editorPane.getId() === NOTEBOOK_DIFF_EDITOR_ID) {
return (editorPane.getControl() as INotebookTextDiffEditor).inlineNotebookEditor;
}
@@ -10,7 +10,6 @@ import { asCssVariable, ColorIdentifier, registerColor } from '../../../../platf
import { ISCMHistoryItem, ISCMHistoryItemGraphNode, ISCMHistoryItemRef, ISCMHistoryItemViewModel } from '../common/history.js';
import { rot } from '../../../../base/common/numbers.js';
import { svgElem } from '../../../../base/browser/dom.js';
import { compareHistoryItemRefs } from './util.js';
export const SWIMLANE_HEIGHT = 22;
export const SWIMLANE_WIDTH = 11;
@@ -365,3 +364,31 @@ export function getHistoryItemIndex(historyItemViewModel: ISCMHistoryItemViewMod
// Circle index - use the input swimlane index if present, otherwise add it to the end
return inputIndex !== -1 ? inputIndex : inputSwimlanes.length;
}
export function compareHistoryItemRefs(
ref1: ISCMHistoryItemRef,
ref2: ISCMHistoryItemRef,
currentHistoryItemRef?: ISCMHistoryItemRef,
currentHistoryItemRemoteRef?: ISCMHistoryItemRef,
currentHistoryItemBaseRef?: ISCMHistoryItemRef
): number {
const getHistoryItemRefOrder = (ref: ISCMHistoryItemRef) => {
if (ref.id === currentHistoryItemRef?.id) {
return 1;
} else if (ref.id === currentHistoryItemRemoteRef?.id) {
return 2;
} else if (ref.id === currentHistoryItemBaseRef?.id) {
return 3;
} else if (ref.color !== undefined) {
return 4;
}
return 99;
};
// Assign order (current > remote > base > color)
const ref1Order = getHistoryItemRefOrder(ref1);
const ref2Order = getHistoryItemRefOrder(ref2);
return ref1Order - ref2Order;
}
@@ -22,7 +22,7 @@ import { IViewsService } from '../../../services/views/common/viewsService.js';
import { IChatWidget, showChatView } from '../../chat/browser/chat.js';
import { IChatContextPickerItem, IChatContextPickerPickItem, IChatContextPickService, picksWithPromiseFn } from '../../chat/browser/chatContextPickService.js';
import { ChatContextKeys } from '../../chat/common/chatContextKeys.js';
import { ISCMHistoryItemVariableEntry } from '../../chat/common/chatModel.js';
import { ISCMHistoryItemVariableEntry } from '../../chat/common/chatVariableEntries.js';
import { ScmHistoryItemResolver } from '../../multiDiffEditor/browser/scmMultiDiffSourceResolver.js';
import { ISCMHistoryItem } from '../common/history.js';
import { ISCMProvider, ISCMService, ISCMViewService } from '../common/scm.js';
+1 -29
View File
@@ -5,7 +5,7 @@
import { localize } from '../../../../nls.js';
import * as platform from '../../../../base/common/platform.js';
import { ISCMHistoryItem, ISCMHistoryItemRef, SCMHistoryItemChangeViewModelTreeElement, SCMHistoryItemLoadMoreTreeElement, SCMHistoryItemViewModelTreeElement } from '../common/history.js';
import { ISCMHistoryItem, SCMHistoryItemChangeViewModelTreeElement, SCMHistoryItemLoadMoreTreeElement, SCMHistoryItemViewModelTreeElement } from '../common/history.js';
import { ISCMResource, ISCMRepository, ISCMResourceGroup, ISCMInput, ISCMActionButton, ISCMViewService, ISCMProvider } from '../common/scm.js';
import { IMenu, MenuItemAction } from '../../../../platform/actions/common/actions.js';
import { IActionViewItemProvider } from '../../../../base/browser/ui/actionbar/actionbar.js';
@@ -224,31 +224,3 @@ export function getHistoryItemHoverContent(themeService: IThemeService, historyI
return { markdown, markdownNotSupportedFallback: historyItem.message };
}
export function compareHistoryItemRefs(
ref1: ISCMHistoryItemRef,
ref2: ISCMHistoryItemRef,
currentHistoryItemRef?: ISCMHistoryItemRef,
currentHistoryItemRemoteRef?: ISCMHistoryItemRef,
currentHistoryItemBaseRef?: ISCMHistoryItemRef
): number {
const getHistoryItemRefOrder = (ref: ISCMHistoryItemRef) => {
if (ref.id === currentHistoryItemRef?.id) {
return 1;
} else if (ref.id === currentHistoryItemRemoteRef?.id) {
return 2;
} else if (ref.id === currentHistoryItemBaseRef?.id) {
return 3;
} else if (ref.color !== undefined) {
return 4;
}
return 99;
};
// Assign order (current > remote > base > color)
const ref1Order = getHistoryItemRefOrder(ref1);
const ref2Order = getHistoryItemRefOrder(ref2);
return ref1Order - ref2Order;
}
@@ -14,7 +14,7 @@ import { IWorkbenchContribution } from '../../../common/contributions.js';
import { getExcludes, IFileQuery, ISearchComplete, ISearchConfiguration, ISearchService, QueryType, VIEW_ID } from '../../../services/search/common/search.js';
import { IViewsService } from '../../../services/views/common/viewsService.js';
import { IChatContextPickerItem, IChatContextPickerPickItem, IChatContextPickService, IChatContextValueItem, picksWithPromiseFn } from '../../chat/browser/chatContextPickService.js';
import { IChatRequestVariableEntry, ISymbolVariableEntry } from '../../chat/common/chatModel.js';
import { IChatRequestVariableEntry, ISymbolVariableEntry } from '../../chat/common/chatVariableEntries.js';
import { SearchContext } from '../common/constants.js';
import { SearchView } from './searchView.js';
import { CancellationToken } from '../../../../base/common/cancellation.js';
@@ -20,7 +20,7 @@ import { StatusbarAlignment, IStatusbarService, IStatusbarEntryAccessor, IStatus
import { IOutputChannelRegistry, Extensions as OutputExt } from '../../../services/output/common/output.js';
import { ITaskEvent, TaskGroup, TaskSettingId, TASKS_CATEGORY, TASK_RUNNING_STATE, TASK_TERMINAL_ACTIVE, TaskEventKind } from '../common/tasks.js';
import { ITaskEvent, TaskGroup, TaskSettingId, TASKS_CATEGORY, TASK_RUNNING_STATE, TASK_TERMINAL_ACTIVE, TaskEventKind, rerunTaskIcon, RerunForActiveTerminalCommandId } from '../common/tasks.js';
import { ITaskService, TaskCommandsRegistered, TaskExecutionSupportedContext } from '../common/taskService.js';
import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry, IWorkbenchContribution, WorkbenchPhase, registerWorkbenchContribution2 } from '../../../common/contributions.js';
@@ -40,8 +40,6 @@ import { TaskDefinitionRegistry } from '../common/taskDefinitionRegistry.js';
import { TerminalMenuBarGroup } from '../../terminal/browser/terminalMenus.js';
import { isString } from '../../../../base/common/types.js';
import { promiseWithResolvers } from '../../../../base/common/async.js';
import { Codicon } from '../../../../base/common/codicons.js';
import { registerIcon } from '../../../../platform/theme/common/iconRegistry.js';
import { TerminalContextKeys } from '../../terminal/common/terminalContextKey.js';
import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js';
@@ -566,8 +564,6 @@ configurationRegistry.registerConfiguration({
}
});
export const rerunTaskIcon = registerIcon('rerun-task', Codicon.refresh, nls.localize('rerunTaskIcon', 'View icon of the rerun task.'));
export const RerunForActiveTerminalCommandId = 'workbench.action.tasks.rerunForActiveTerminal';
registerAction2(class extends Action2 {
constructor() {
super({
@@ -595,4 +591,3 @@ registerAction2(class extends Action2 {
}
}
});
@@ -40,7 +40,7 @@ import { TaskTerminalStatus } from './taskTerminalStatus.js';
import { ProblemCollectorEventKind, ProblemHandlingStrategy, StartStopProblemCollector, WatchingProblemCollector } from '../common/problemCollectors.js';
import { GroupKind } from '../common/taskConfiguration.js';
import { IResolveSet, IResolvedVariables, ITaskExecuteResult, ITaskResolver, ITaskSummary, ITaskSystem, ITaskSystemInfo, ITaskSystemInfoResolver, ITaskTerminateResponse, TaskError, TaskErrors, TaskExecuteKind, Triggers } from '../common/taskSystem.js';
import { CommandOptions, CommandString, ContributedTask, CustomTask, DependsOrder, ICommandConfiguration, IConfigurationProperties, IExtensionTaskSource, IPresentationOptions, IShellConfiguration, IShellQuotingOptions, ITaskEvent, InMemoryTask, PanelKind, RevealKind, RevealProblemKind, RuntimeType, ShellQuoting, TASK_TERMINAL_ACTIVE, Task, TaskEvent, TaskEventKind, TaskScope, TaskSourceKind } from '../common/tasks.js';
import { CommandOptions, CommandString, ContributedTask, CustomTask, DependsOrder, ICommandConfiguration, IConfigurationProperties, IExtensionTaskSource, IPresentationOptions, IShellConfiguration, IShellQuotingOptions, ITaskEvent, InMemoryTask, PanelKind, RerunForActiveTerminalCommandId, RevealKind, RevealProblemKind, RuntimeType, ShellQuoting, TASK_TERMINAL_ACTIVE, Task, TaskEvent, TaskEventKind, TaskScope, TaskSourceKind, rerunTaskIcon } from '../common/tasks.js';
import { ITerminalGroupService, ITerminalInstance, ITerminalService } from '../../terminal/browser/terminal.js';
import { VSCodeOscProperty, VSCodeOscPt, VSCodeSequence } from '../../terminal/browser/terminalEscapeSequences.js';
import { TerminalProcessExtHostProxy } from '../../terminal/browser/terminalProcessExtHostProxy.js';
@@ -50,7 +50,6 @@ import { IWorkbenchEnvironmentService } from '../../../services/environment/comm
import { IOutputService } from '../../../services/output/common/output.js';
import { IPaneCompositePartService } from '../../../services/panecomposite/browser/panecomposite.js';
import { IPathService } from '../../../services/path/common/pathService.js';
import { RerunForActiveTerminalCommandId, rerunTaskIcon } from './task.contribution.js';
import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
interface ITerminalData {

Some files were not shown because too many files have changed in this diff Show More