Check for cyclic dependencies during compile (#235808)

* Check for cyclic dependencies during compile

Changes gulp-tsb to check the emitted JS code for cyclic dependencies. Historically we never cared about cycles between TS files as long as they dissappeared after compile (e.g type-dependencies, not runtime dependencies)

https://github.com/microsoft/vscode-internalbacklog/issues/5271

* fix cycling dependencies

fyi @aeschli @aiday-mar

* remove cyclic dependency with unused `BasedTextEdit` fyi @hediet

* remove cycle between chatEditService and chatEditingSession

fyi @alexdima

* remove cyclic dependency between chatSetup and chatViewPane

fyi @roblourens

* better cycle detection

* don't check cycles when not needed

* clear graph when reprocessing file dependencies

* remove cycle between with `notebookChatEditController` fyi @DonJayamanne

* modernize and cleanup tsb/utils
This commit is contained in:
Johannes Rieken
2024-12-11 11:26:59 +01:00
committed by GitHub
parent 1b4662bc51
commit a40e594f25
18 changed files with 359 additions and 309 deletions

View File

@@ -26,6 +26,8 @@ function normalize(path) {
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 service = ts.createLanguageService(host, ts.createDocumentRegistry());
const lastBuildVersion = Object.create(null);
const lastDtsHash = Object.create(null);
@@ -251,6 +253,11 @@ function createTypeScriptBuilder(config, projectFile, cmd) {
lastDtsHash[fileName] = value.signature;
filesWithChangedSignature.push(fileName);
}
// line up for cycle check
const jsValue = value.files.find(candidate => candidate.basename.endsWith('.js'));
if (jsValue) {
outHost.addScriptSnapshot(jsValue.path, new ScriptSnapshot(String(jsValue.contents), new Date()));
}
}).catch(e => {
// can't just skip this or make a result up..
host.error(`ERROR emitting ${fileName}`);
@@ -341,16 +348,37 @@ 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: ts.DiagnosticCategory.Error,
code: 1,
file: undefined,
start: undefined,
length: undefined,
messageText: `CYCLIC dependency between ${oneCycle}`
};
onError(cycleError);
newErrors[projectFile] = [cycleError];
}
}).then(() => {
// store the build versions to not rebuilt the next time
newLastBuildVersion.forEach((value, key) => {
lastBuildVersion[key] = value;
});
// print old errors and keep them
utils.collections.forEach(oldErrors, entry => {
entry.value.forEach(diag => onError(diag));
newErrors[entry.key] = entry.value;
});
for (const [key, value] of Object.entries(oldErrors)) {
value.forEach(diag => onError(diag));
newErrors[key] = value;
}
oldErrors = newErrors;
// print stats
const headNow = process.memoryUsage().heapUsed;
@@ -415,7 +443,7 @@ class LanguageServiceHost {
this._snapshots = Object.create(null);
this._filesInProject = new Set(_cmdLine.fileNames);
this._filesAdded = new Set();
this._dependencies = new utils.graph.Graph(s => s);
this._dependencies = new utils.graph.Graph();
this._dependenciesRecomputeList = [];
this._fileNameToDeclaredModule = Object.create(null);
this._projectVersion = 1;
@@ -478,10 +506,6 @@ class LanguageServiceHost {
}
if (!old || old.getVersion() !== snapshot.getVersion()) {
this._dependenciesRecomputeList.push(filename);
const node = this._dependencies.lookup(filename);
if (node) {
node.outgoing = Object.create(null);
}
// (cheap) check for declare module
LanguageServiceHost._declareModule.lastIndex = 0;
let match;
@@ -523,9 +547,19 @@ class LanguageServiceHost {
filename = normalize(filename);
const node = this._dependencies.lookup(filename);
if (node) {
utils.collections.forEach(node.incoming, entry => target.push(entry.key));
node.incoming.forEach(entry => target.push(entry.data));
}
}
hasCyclicDependency() {
// 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;
}
_processFile(filename) {
if (filename.match(/.*\.d\.ts$/)) {
return;
@@ -537,6 +571,8 @@ class LanguageServiceHost {
return;
}
const info = ts.preProcessFile(snapshot.getText(0, snapshot.getLength()), true);
// (0) clear out old dependencies
this._dependencies.resetNode(filename);
// (1) ///-references
info.referencedFiles.forEach(ref => {
const resolvedPath = path.resolve(path.dirname(filename), ref.fileName);
@@ -545,6 +581,10 @@ class LanguageServiceHost {
});
// (2) import-require statements
info.importedFiles.forEach(ref => {
if (!ref.fileName.startsWith('.') || path.extname(ref.fileName) === '') {
// node module?
return;
}
const stopDirname = normalize(this.getCurrentDirectory());
let dirname = filename;
let found = false;
@@ -563,6 +603,10 @@ class LanguageServiceHost {
this._dependencies.inertEdge(filename, normalizedPath + '.d.ts');
found = true;
}
else if (this.getScriptSnapshot(normalizedPath + '.js')) {
this._dependencies.inertEdge(filename, normalizedPath + '.js');
found = true;
}
}
if (!found) {
for (const key in this._fileNameToDeclaredModule) {

View File

@@ -42,6 +42,10 @@ export function createTypeScriptBuilder(config: IConfiguration, projectFile: str
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: string;
const service = ts.createLanguageService(host, ts.createDocumentRegistry());
const lastBuildVersion: { [path: string]: string } = Object.create(null);
const lastDtsHash: { [path: string]: string } = Object.create(null);
@@ -305,6 +309,13 @@ export function createTypeScriptBuilder(config: IConfiguration, projectFile: str
lastDtsHash[fileName] = value.signature;
filesWithChangedSignature.push(fileName);
}
// line up for cycle check
const jsValue = value.files.find(candidate => candidate.basename.endsWith('.js'));
if (jsValue) {
outHost.addScriptSnapshot(jsValue.path, new ScriptSnapshot(String(jsValue.contents), new Date()));
}
}).catch(e => {
// can't just skip this or make a result up..
host.error(`ERROR emitting ${fileName}`);
@@ -389,6 +400,7 @@ export function createTypeScriptBuilder(config: IConfiguration, projectFile: str
}
}
// (last) done
else {
resolve();
@@ -410,16 +422,40 @@ export function createTypeScriptBuilder(config: IConfiguration, projectFile: str
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: ts.Diagnostic = {
category: ts.DiagnosticCategory.Error,
code: 1,
file: undefined,
start: undefined,
length: undefined,
messageText: `CYCLIC dependency between ${oneCycle}`
};
onError(cycleError);
newErrors[projectFile] = [cycleError];
}
}).then(() => {
// store the build versions to not rebuilt the next time
newLastBuildVersion.forEach((value, key) => {
lastBuildVersion[key] = value;
});
// print old errors and keep them
utils.collections.forEach(oldErrors, entry => {
entry.value.forEach(diag => onError(diag));
newErrors[entry.key] = entry.value;
});
for (const [key, value] of Object.entries(oldErrors)) {
value.forEach(diag => onError(diag));
newErrors[key] = value;
}
oldErrors = newErrors;
// print stats
@@ -503,7 +539,7 @@ class LanguageServiceHost implements ts.LanguageServiceHost {
this._snapshots = Object.create(null);
this._filesInProject = new Set(_cmdLine.fileNames);
this._filesAdded = new Set();
this._dependencies = new utils.graph.Graph<string>(s => s);
this._dependencies = new utils.graph.Graph<string>();
this._dependenciesRecomputeList = [];
this._fileNameToDeclaredModule = Object.create(null);
@@ -576,10 +612,6 @@ class LanguageServiceHost implements ts.LanguageServiceHost {
}
if (!old || old.getVersion() !== snapshot.getVersion()) {
this._dependenciesRecomputeList.push(filename);
const node = this._dependencies.lookup(filename);
if (node) {
node.outgoing = Object.create(null);
}
// (cheap) check for declare module
LanguageServiceHost._declareModule.lastIndex = 0;
@@ -628,10 +660,21 @@ class LanguageServiceHost implements ts.LanguageServiceHost {
filename = normalize(filename);
const node = this._dependencies.lookup(filename);
if (node) {
utils.collections.forEach(node.incoming, entry => target.push(entry.key));
node.incoming.forEach(entry => target.push(entry.data));
}
}
hasCyclicDependency(): 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;
}
_processFile(filename: string): void {
if (filename.match(/.*\.d\.ts$/)) {
return;
@@ -644,6 +687,9 @@ class LanguageServiceHost implements ts.LanguageServiceHost {
}
const info = ts.preProcessFile(snapshot.getText(0, snapshot.getLength()), true);
// (0) clear out old dependencies
this._dependencies.resetNode(filename);
// (1) ///-references
info.referencedFiles.forEach(ref => {
const resolvedPath = path.resolve(path.dirname(filename), ref.fileName);
@@ -654,10 +700,18 @@ class LanguageServiceHost implements ts.LanguageServiceHost {
// (2) import-require statements
info.importedFiles.forEach(ref => {
if (!ref.fileName.startsWith('.') || path.extname(ref.fileName) === '') {
// node module?
return;
}
const stopDirname = normalize(this.getCurrentDirectory());
let dirname = filename;
let found = false;
while (!found && dirname.indexOf(stopDirname) === 0) {
dirname = path.dirname(dirname);
let resolvedPath = path.resolve(dirname, ref.fileName);
@@ -673,6 +727,10 @@ class LanguageServiceHost implements ts.LanguageServiceHost {
} else if (this.getScriptSnapshot(normalizedPath + '.d.ts')) {
this._dependencies.inertEdge(filename, normalizedPath + '.d.ts');
found = true;
} else if (this.getScriptSnapshot(normalizedPath + '.js')) {
this._dependencies.inertEdge(filename, normalizedPath + '.js');
found = true;
}
}

View File

@@ -4,54 +4,9 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.graph = exports.strings = exports.collections = void 0;
var collections;
(function (collections) {
const hasOwnProperty = Object.prototype.hasOwnProperty;
function lookup(collection, key) {
if (hasOwnProperty.call(collection, key)) {
return collection[key];
}
return null;
}
collections.lookup = lookup;
function insert(collection, key, value) {
collection[key] = value;
}
collections.insert = insert;
function lookupOrInsert(collection, key, value) {
if (hasOwnProperty.call(collection, key)) {
return collection[key];
}
else {
collection[key] = value;
return value;
}
}
collections.lookupOrInsert = lookupOrInsert;
function forEach(collection, callback) {
for (const key in collection) {
if (hasOwnProperty.call(collection, key)) {
callback({
key: key,
value: collection[key]
});
}
}
}
collections.forEach = forEach;
function contains(collection, key) {
return hasOwnProperty.call(collection, key);
}
collections.contains = contains;
})(collections || (exports.collections = collections = {}));
exports.graph = exports.strings = void 0;
var strings;
(function (strings) {
/**
* The empty string. The one and only.
*/
strings.empty = '';
strings.eolUnix = '\r\n';
function format(value, ...rest) {
return value.replace(/({\d+})/g, function (match) {
const index = Number(match.substring(1, match.length - 1));
@@ -62,63 +17,83 @@ var strings;
})(strings || (exports.strings = strings = {}));
var graph;
(function (graph) {
function newNode(data) {
return {
data: data,
incoming: {},
outgoing: {}
};
class Node {
data;
incoming = new Map();
outgoing = new Map();
constructor(data) {
this.data = data;
}
}
graph.newNode = newNode;
graph.Node = Node;
class Graph {
_hashFn;
_nodes = {};
constructor(_hashFn) {
this._hashFn = _hashFn;
// empty
}
traverse(start, inwards, callback) {
const startNode = this.lookup(start);
if (!startNode) {
return;
}
this._traverse(startNode, inwards, {}, callback);
}
_traverse(node, inwards, seen, callback) {
const key = this._hashFn(node.data);
if (collections.contains(seen, key)) {
return;
}
seen[key] = true;
callback(node.data);
const nodes = inwards ? node.outgoing : node.incoming;
collections.forEach(nodes, (entry) => this._traverse(entry.value, inwards, seen, callback));
}
_nodes = new Map();
inertEdge(from, to) {
const fromNode = this.lookupOrInsertNode(from);
const toNode = this.lookupOrInsertNode(to);
fromNode.outgoing[this._hashFn(to)] = toNode;
toNode.incoming[this._hashFn(from)] = fromNode;
fromNode.outgoing.set(toNode.data, toNode);
toNode.incoming.set(fromNode.data, fromNode);
}
removeNode(data) {
const key = this._hashFn(data);
delete this._nodes[key];
collections.forEach(this._nodes, (entry) => {
delete entry.value.outgoing[key];
delete entry.value.incoming[key];
});
resetNode(data) {
const node = this._nodes.get(data);
if (!node) {
return;
}
for (const outDep of node.outgoing.values()) {
outDep.incoming.delete(node.data);
}
node.outgoing.clear();
}
lookupOrInsertNode(data) {
const key = this._hashFn(data);
let node = collections.lookup(this._nodes, key);
let node = this._nodes.get(data);
if (!node) {
node = newNode(data);
this._nodes[key] = node;
node = new Node(data);
this._nodes.set(data, node);
}
return node;
}
lookup(data) {
return collections.lookup(this._nodes, this._hashFn(data));
return this._nodes.get(data) ?? null;
}
findCycle() {
let result;
let foundStartNodes = false;
const checked = new Set();
for (const [_start, value] of this._nodes) {
if (Object.values(value.incoming).length > 0) {
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());
if (result) {
break;
}
}
if (!foundStartNodes) {
// everything is a cycle
return Array.from(this._nodes.keys());
}
return result;
}
}
graph.Graph = Graph;

View File

@@ -3,54 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export module collections {
const hasOwnProperty = Object.prototype.hasOwnProperty;
export function lookup<T>(collection: { [keys: string]: T }, key: string): T | null {
if (hasOwnProperty.call(collection, key)) {
return collection[key];
}
return null;
}
export function insert<T>(collection: { [keys: string]: T }, key: string, value: T): void {
collection[key] = value;
}
export function lookupOrInsert<T>(collection: { [keys: string]: T }, key: string, value: T): T {
if (hasOwnProperty.call(collection, key)) {
return collection[key];
} else {
collection[key] = value;
return value;
}
}
export function forEach<T>(collection: { [keys: string]: T }, callback: (entry: { key: string; value: T }) => void): void {
for (const key in collection) {
if (hasOwnProperty.call(collection, key)) {
callback({
key: key,
value: collection[key]
});
}
}
}
export function contains(collection: { [keys: string]: any }, key: string): boolean {
return hasOwnProperty.call(collection, key);
}
}
export module strings {
/**
* The empty string. The one and only.
*/
export const empty = '';
export const eolUnix = '\r\n';
export namespace strings {
export function format(value: string, ...rest: any[]): string {
return value.replace(/({\d+})/g, function (match) {
@@ -60,80 +13,104 @@ export module strings {
}
}
export module graph {
export namespace graph {
export interface Node<T> {
data: T;
incoming: { [key: string]: Node<T> };
outgoing: { [key: string]: Node<T> };
}
export class Node<T> {
export function newNode<T>(data: T): Node<T> {
return {
data: data,
incoming: {},
outgoing: {}
};
readonly incoming = new Map<T, Node<T>>();
readonly outgoing = new Map<T, Node<T>>();
constructor(readonly data: T) {
}
}
export class Graph<T> {
private _nodes: { [key: string]: Node<T> } = {};
constructor(private _hashFn: (element: T) => string) {
// empty
}
traverse(start: T, inwards: boolean, callback: (data: T) => void): void {
const startNode = this.lookup(start);
if (!startNode) {
return;
}
this._traverse(startNode, inwards, {}, callback);
}
private _traverse(node: Node<T>, inwards: boolean, seen: { [key: string]: boolean }, callback: (data: T) => void): void {
const key = this._hashFn(node.data);
if (collections.contains(seen, key)) {
return;
}
seen[key] = true;
callback(node.data);
const nodes = inwards ? node.outgoing : node.incoming;
collections.forEach(nodes, (entry) => this._traverse(entry.value, inwards, seen, callback));
}
private _nodes = new Map<T, Node<T>>();
inertEdge(from: T, to: T): void {
const fromNode = this.lookupOrInsertNode(from);
const toNode = this.lookupOrInsertNode(to);
fromNode.outgoing[this._hashFn(to)] = toNode;
toNode.incoming[this._hashFn(from)] = fromNode;
fromNode.outgoing.set(toNode.data, toNode);
toNode.incoming.set(fromNode.data, fromNode);
}
removeNode(data: T): void {
const key = this._hashFn(data);
delete this._nodes[key];
collections.forEach(this._nodes, (entry) => {
delete entry.value.outgoing[key];
delete entry.value.incoming[key];
});
resetNode(data: T): void {
const node = this._nodes.get(data);
if (!node) {
return;
}
for (const outDep of node.outgoing.values()) {
outDep.incoming.delete(node.data);
}
node.outgoing.clear();
}
lookupOrInsertNode(data: T): Node<T> {
const key = this._hashFn(data);
let node = collections.lookup(this._nodes, key);
let node = this._nodes.get(data);
if (!node) {
node = newNode(data);
this._nodes[key] = node;
node = new Node(data);
this._nodes.set(data, node);
}
return node;
}
lookup(data: T): Node<T> | null {
return collections.lookup(this._nodes, this._hashFn(data));
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) {
continue;
}
foundStartNodes = true;
const dfs = (node: Node<T>, visited: Set<Node<T>>) => {
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());
if (result) {
break;
}
}
if (!foundStartNodes) {
// everything is a cycle
return Array.from(this._nodes.keys());
}
return result;
}
}

View File

@@ -8,7 +8,6 @@ import { assert, assertFn, checkAdjacentItems } from '../../../base/common/asser
import { BugIndicatingError } from '../../../base/common/errors.js';
import { commonPrefixLength, commonSuffixLength, splitLines } from '../../../base/common/strings.js';
import { ISingleEditOperation } from './editOperation.js';
import { LineEdit } from './lineEdit.js';
import { LineRange } from './lineRange.js';
import { OffsetEdit } from './offsetEdit.js';
import { Position } from './position.js';
@@ -399,16 +398,3 @@ export class StringText extends AbstractText {
return this._t.textLength;
}
}
export class BasedTextEdit {
constructor(
public readonly base: AbstractText,
public readonly edit: TextEdit,
) {
}
toString() {
const lineEdit = LineEdit.fromTextEdit(this.edit, this.base);
return lineEdit.humanReadablePatch(this.base.getLines());
}
}

View File

@@ -0,0 +1,15 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IPartialEditorMouseEvent, MouseTargetType } from '../../../../browser/editorBrowser.js';
import { ColorDecorationInjectedTextMarker } from '../colorDetector.js';
export function isOnColorDecorator(mouseEvent: IPartialEditorMouseEvent): boolean {
const target = mouseEvent.target;
return !!target
&& target.type === MouseTargetType.CONTENT_TEXT
&& target.detail.injectedText?.options.attachedData === ColorDecorationInjectedTextMarker;
}

View File

@@ -4,13 +4,13 @@
*--------------------------------------------------------------------------------------------*/
import { Disposable } from '../../../../../base/common/lifecycle.js';
import { ICodeEditor, IEditorMouseEvent, IPartialEditorMouseEvent, MouseTargetType } from '../../../../browser/editorBrowser.js';
import { ICodeEditor, IEditorMouseEvent } from '../../../../browser/editorBrowser.js';
import { EditorOption } from '../../../../common/config/editorOptions.js';
import { Range } from '../../../../common/core/range.js';
import { IEditorContribution } from '../../../../common/editorCommon.js';
import { ColorDecorationInjectedTextMarker } from '../colorDetector.js';
import { ContentHoverController } from '../../../hover/browser/contentHoverController.js';
import { HoverStartMode, HoverStartSource } from '../../../hover/browser/hoverOperation.js';
import { isOnColorDecorator } from './hoverColorPicker.js';
export class HoverColorPickerContribution extends Disposable implements IEditorContribution {
@@ -52,10 +52,3 @@ export class HoverColorPickerContribution extends Disposable implements IEditorC
hoverController.showContentHover(range, HoverStartMode.Immediate, HoverStartSource.Click, false);
}
}
export function isOnColorDecorator(mouseEvent: IPartialEditorMouseEvent): boolean {
const target = mouseEvent.target;
return !!target
&& target.type === MouseTargetType.CONTENT_TEXT
&& target.detail.injectedText?.options.attachedData === ColorDecorationInjectedTextMarker;
}

View File

@@ -22,7 +22,7 @@ import { isMousePositionWithinElement } from './hoverUtils.js';
import { ContentHoverWidgetWrapper } from './contentHoverWidgetWrapper.js';
import './hover.css';
import { Emitter } from '../../../../base/common/event.js';
import { isOnColorDecorator } from '../../colorPicker/browser/hoverColorPicker/hoverColorPickerContribution.js';
import { isOnColorDecorator } from '../../colorPicker/browser/hoverColorPicker/hoverColorPicker.js';
// sticky hover widget which doesn't disappear on focus out and such
const _sticky = false

View File

@@ -3,6 +3,8 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { AsyncIterableObject } from '../../../../../base/common/async.js';
import { CancellationToken } from '../../../../../base/common/cancellation.js';
import { Codicon } from '../../../../../base/common/codicons.js';
import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js';
import { ICodeEditor } from '../../../../../editor/browser/editorBrowser.js';
@@ -20,6 +22,7 @@ import { TerminalLocation } from '../../../../../platform/terminal/common/termin
import { IUntitledTextResourceEditorInput } from '../../../../common/editor.js';
import { IEditorService } from '../../../../services/editor/common/editorService.js';
import { accessibleViewInCodeBlock } from '../../../accessibility/browser/accessibilityConfiguration.js';
import { InlineChatController } from '../../../inlineChat/browser/inlineChatController.js';
import { ITerminalEditorService, ITerminalGroupService, ITerminalService } from '../../../terminal/browser/terminal.js';
import { ChatAgentLocation } from '../../common/chatAgents.js';
import { ChatContextKeys } from '../../common/chatContextKeys.js';
@@ -529,10 +532,38 @@ export function registerChatCodeCompareBlockActions() {
async runWithContext(accessor: ServicesAccessor, context: ICodeCompareBlockActionContext): Promise<any> {
const instaService = accessor.get(IInstantiationService);
const editorService = accessor.get(ICodeEditorService);
const editor = instaService.createInstance(DefaultChatTextEditor);
await editor.preview(context.element, context.edit);
const item = context.edit;
const response = context.element;
if (item.state?.applied) {
// already applied
return false;
}
if (!response.response.value.includes(item)) {
// bogous item
return false;
}
const firstEdit = item.edits[0]?.[0];
if (!firstEdit) {
return false;
}
const textEdits = AsyncIterableObject.fromArray(item.edits);
const editorToApply = await editorService.openCodeEditor({ resource: item.uri }, null);
if (editorToApply) {
const inlineChatController = InlineChatController.get(editorToApply);
if (inlineChatController) {
editorToApply.revealLineInCenterIfOutsideViewport(firstEdit.range.startLineNumber);
inlineChatController.reviewEdits(firstEdit.range, textEdits, CancellationToken.None);
response.setEditApplied(item, 1);
return true;
}
}
return false;
}
});

View File

@@ -487,14 +487,6 @@ class ChatDecorationsProvider extends Disposable implements IDecorationsProvider
}
export class ChatEditingMultiDiffSourceResolver implements IMultiDiffSourceResolver {
public static readonly scheme = CHAT_EDITING_MULTI_DIFF_SOURCE_RESOLVER_SCHEME;
public static getMultiDiffSourceUri(): URI {
return URI.from({
scheme: ChatEditingMultiDiffSourceResolver.scheme,
path: '',
});
}
constructor(
private readonly _currentSession: IObservable<ChatEditingSession | null>,
@@ -502,7 +494,7 @@ export class ChatEditingMultiDiffSourceResolver implements IMultiDiffSourceResol
) { }
canHandleUri(uri: URI): boolean {
return uri.scheme === ChatEditingMultiDiffSourceResolver.scheme;
return uri.scheme === CHAT_EDITING_MULTI_DIFF_SOURCE_RESOLVER_SCHEME;
}
async resolveDiffSource(uri: URI): Promise<IResolvedMultiDiffSource> {

View File

@@ -30,9 +30,8 @@ import { IEditorService } from '../../../../services/editor/common/editorService
import { MultiDiffEditor } from '../../../multiDiffEditor/browser/multiDiffEditor.js';
import { MultiDiffEditorInput } from '../../../multiDiffEditor/browser/multiDiffEditorInput.js';
import { ChatAgentLocation, IChatAgentService } from '../../common/chatAgents.js';
import { ChatEditingSessionChangeType, ChatEditingSessionState, ChatEditKind, IChatEditingSession, IModifiedFileEntry, WorkingSetDisplayMetadata, WorkingSetEntryRemovalReason, WorkingSetEntryState } from '../../common/chatEditingService.js';
import { ChatEditingSessionChangeType, ChatEditingSessionState, ChatEditKind, getMultiDiffSourceUri, IChatEditingSession, IModifiedFileEntry, WorkingSetDisplayMetadata, WorkingSetEntryRemovalReason, WorkingSetEntryState } from '../../common/chatEditingService.js';
import { IChatResponseModel } from '../../common/chatModel.js';
import { ChatEditingMultiDiffSourceResolver } from './chatEditingService.js';
import { ChatEditingModifiedFileEntry, IModifiedEntryTelemetryInfo, ISnapshotEntry } from './chatEditingModifiedFileEntry.js';
import { ChatEditingTextModelContentProvider } from './chatEditingTextModelContentProviders.js';
import { Schemas } from '../../../../../base/common/network.js';
@@ -484,7 +483,7 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio
}
}
const input = MultiDiffEditorInput.fromResourceMultiDiffEditorInput({
multiDiffSource: ChatEditingMultiDiffSourceResolver.getMultiDiffSourceUri(),
multiDiffSource: getMultiDiffSourceUri(),
label: localize('multiDiffEditorInput.name', "Suggested Edits")
}, this._instantiationService);

View File

@@ -19,7 +19,7 @@ import { ChatContextKeys } from '../common/chatContextKeys.js';
import { isEqual } from '../../../../base/common/resources.js';
import { Range } from '../../../../editor/common/core/range.js';
import { getNotebookEditorFromEditorPane } from '../../notebook/browser/notebookBrowser.js';
import { ctxNotebookHasEditorModification } from '../../notebook/browser/contrib/chatEdit/notebookChatEditController.js';
import { ctxNotebookHasEditorModification } from '../../notebook/browser/contrib/chatEdit/notebookChatEditContext.js';
abstract class NavigateAction extends Action2 {

View File

@@ -52,7 +52,7 @@ import { IChatAgentService } from '../common/chatAgents.js';
import { ChatContextKeys } from '../common/chatContextKeys.js';
import { CHAT_CATEGORY } from './actions/chatActions.js';
import { ChatViewId, EditsViewId, ensureSideBarChatViewSize, IChatWidget, showChatView, showEditsView } from './chat.js';
import { CHAT_EDITING_SIDEBAR_PANEL_ID, CHAT_SIDEBAR_PANEL_ID } from './chatViewPane.js';
import { CHAT_EDITING_SIDEBAR_PANEL_ID, CHAT_SIDEBAR_PANEL_ID, SetupWelcomeViewCondition } from './chatViewPane.js';
import { ChatViewsWelcomeExtensions, IChatViewsWelcomeContributionRegistry } from './viewsWelcome/chatViewsWelcome.js';
import { IChatQuotasService } from './chatQuotasService.js';
import { mainWindow } from '../../../../base/browser/window.js';
@@ -95,25 +95,6 @@ enum ChatEntitlement {
const TRIGGER_SETUP_COMMAND_ID = 'workbench.action.chat.triggerSetup';
const TRIGGER_SETUP_COMMAND_LABEL = localize2('triggerChatSetup', "Use AI Features with Copilot for Free...");
export const SetupWelcomeViewKeys = new Set([ChatContextKeys.Setup.triggered.key, ChatContextKeys.Setup.installed.key, ChatContextKeys.Setup.signedOut.key, ChatContextKeys.Setup.canSignUp.key]);
export const SetupWelcomeViewCondition = ContextKeyExpr.and(
ContextKeyExpr.has('config.chat.experimental.offerSetup'),
ContextKeyExpr.or(
ContextKeyExpr.and(
ChatContextKeys.Setup.triggered,
ChatContextKeys.Setup.installed.negate()
),
ContextKeyExpr.and(
ChatContextKeys.Setup.canSignUp,
ChatContextKeys.Setup.installed
),
ContextKeyExpr.and(
ChatContextKeys.Setup.signedOut,
ChatContextKeys.Setup.installed
)
)
)!;
export class ChatSetupContribution extends Disposable implements IWorkbenchContribution {
static readonly ID = 'workbench.chat.setup';

View File

@@ -8,7 +8,7 @@ import { CancellationToken } from '../../../../base/common/cancellation.js';
import { DisposableStore } from '../../../../base/common/lifecycle.js';
import { MarshalledId } from '../../../../base/common/marshallingIds.js';
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
import { ContextKeyExpr, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js';
import { IHoverService } from '../../../../platform/hover/browser/hover.js';
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
@@ -27,10 +27,10 @@ import { SIDE_BAR_FOREGROUND } from '../../../common/theme.js';
import { IViewDescriptorService } from '../../../common/views.js';
import { IChatViewTitleActionContext } from '../common/chatActions.js';
import { ChatAgentLocation, IChatAgentService } from '../common/chatAgents.js';
import { ChatContextKeys } from '../common/chatContextKeys.js';
import { ChatModelInitState, IChatModel } from '../common/chatModel.js';
import { CHAT_PROVIDER_ID } from '../common/chatParticipantContribTypes.js';
import { IChatService } from '../common/chatService.js';
import { SetupWelcomeViewCondition, SetupWelcomeViewKeys } from './chatSetup.js';
import { ChatWidget, IChatViewState } from './chatWidget.js';
import { ChatViewWelcomeController, IViewWelcomeDelegate } from './viewsWelcome/chatViewWelcomeController.js';
@@ -279,3 +279,22 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate {
}
}
}
export const SetupWelcomeViewKeys = new Set([ChatContextKeys.Setup.triggered.key, ChatContextKeys.Setup.installed.key, ChatContextKeys.Setup.signedOut.key, ChatContextKeys.Setup.canSignUp.key]);
export const SetupWelcomeViewCondition = ContextKeyExpr.and(
ContextKeyExpr.has('config.chat.experimental.offerSetup'),
ContextKeyExpr.or(
ContextKeyExpr.and(
ChatContextKeys.Setup.triggered,
ChatContextKeys.Setup.installed.negate()
),
ContextKeyExpr.and(
ChatContextKeys.Setup.canSignUp,
ChatContextKeys.Setup.installed
),
ContextKeyExpr.and(
ChatContextKeys.Setup.signedOut,
ChatContextKeys.Setup.installed
)
)
)!;

View File

@@ -8,7 +8,7 @@ import './codeBlockPart.css';
import * as dom from '../../../../base/browser/dom.js';
import { renderFormattedText } from '../../../../base/browser/formattedTextRenderer.js';
import { Button } from '../../../../base/browser/ui/button/button.js';
import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js';
import { CancellationToken } from '../../../../base/common/cancellation.js';
import { Codicon } from '../../../../base/common/codicons.js';
import { Emitter, Event } from '../../../../base/common/event.js';
import { combinedDisposable, Disposable, MutableDisposable } from '../../../../base/common/lifecycle.js';
@@ -69,8 +69,6 @@ import { ChatTreeItem } from './chat.js';
import { IChatRendererDelegate } from './chatListRenderer.js';
import { ChatEditorOptions } from './chatOptions.js';
import { emptyProgressRunner, IEditorProgressService } from '../../../../platform/progress/common/progress.js';
import { AsyncIterableObject } from '../../../../base/common/async.js';
import { InlineChatController } from '../../inlineChat/browser/inlineChatController.js';
const $ = dom.$;
@@ -941,37 +939,5 @@ export class DefaultChatTextEditor {
response.setEditApplied(item, -1);
}
async preview(response: IChatResponseModel | IChatResponseViewModel, item: IChatTextEditGroup) {
if (item.state?.applied) {
// already applied
return false;
}
if (!response.response.value.includes(item)) {
// bogous item
return false;
}
const firstEdit = item.edits[0]?.[0];
if (!firstEdit) {
return false;
}
const textEdits = AsyncIterableObject.fromArray(item.edits);
const editorToApply = await this.editorService.openCodeEditor({ resource: item.uri }, null);
if (editorToApply) {
const inlineChatController = InlineChatController.get(editorToApply);
if (inlineChatController) {
const tokenSource = new CancellationTokenSource();
editorToApply.revealLineInCenterIfOutsideViewport(firstEdit.range.startLineNumber);
const promise = inlineChatController.reviewEdits(firstEdit.range, textEdits, tokenSource.token);
response.setEditApplied(item, 1);
promise.finally(() => {
tokenSource.dispose();
});
return true;
}
}
return false;
}
}

View File

@@ -163,3 +163,10 @@ export interface IChatEditingActionContext {
export function isChatEditingActionContext(thing: unknown): thing is IChatEditingActionContext {
return typeof thing === 'object' && !!thing && 'sessionId' in thing;
}
export function getMultiDiffSourceUri(): URI {
return URI.from({
scheme: CHAT_EDITING_MULTI_DIFF_SOURCE_RESOLVER_SCHEME,
path: '',
});
}

View File

@@ -0,0 +1,9 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { localize } from '../../../../../../nls.js';
import { RawContextKey } from '../../../../../../platform/contextkey/common/contextkey.js';
export const ctxNotebookHasEditorModification = new RawContextKey<boolean>('chat.hasNotebookEditorModifications', undefined, localize('chat.hasNotebookEditorModifications', "The current Notebook editor contains chat modifications"));

View File

@@ -17,14 +17,12 @@ import { INotebookOriginalModelReferenceFactory, NotebookOriginalModelReferenceF
import { debouncedObservable2 } from '../../../../../../base/common/observableInternal/utils.js';
import { CellDiffInfo } from '../../diff/notebookDiffViewModel.js';
import { NotebookChatActionsOverlayController } from './notebookChatActionsOverlay.js';
import { IContextKey, IContextKeyService, RawContextKey } from '../../../../../../platform/contextkey/common/contextkey.js';
import { localize } from '../../../../../../nls.js';
import { IContextKey, IContextKeyService } from '../../../../../../platform/contextkey/common/contextkey.js';
import { registerNotebookContribution } from '../../notebookEditorExtensions.js';
import { InstantiationType, registerSingleton } from '../../../../../../platform/instantiation/common/extensions.js';
import { INotebookOriginalCellModelFactory, OriginalNotebookCellModelFactory } from './notebookOriginalCellModelFactory.js';
import { Event } from '../../../../../../base/common/event.js';
export const ctxNotebookHasEditorModification = new RawContextKey<boolean>('chat.hasNotebookEditorModifications', undefined, localize('chat.hasNotebookEditorModifications', "The current Notebook editor contains chat modifications"));
import { ctxNotebookHasEditorModification } from './notebookChatEditContext.js';
export class NotebookChatEditorControllerContrib extends Disposable implements INotebookEditorContribution {