@theia/ai-chat 1.75.0-next.21 → 1.75.0-next.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/browser/ai-chat-frontend-module.d.ts.map +1 -1
- package/lib/browser/ai-chat-frontend-module.js +4 -0
- package/lib/browser/ai-chat-frontend-module.js.map +1 -1
- package/lib/browser/change-set-file-element.d.ts +2 -0
- package/lib/browser/change-set-file-element.d.ts.map +1 -1
- package/lib/browser/change-set-file-element.js +10 -4
- package/lib/browser/change-set-file-element.js.map +1 -1
- package/lib/browser/file-read-tracker-impl.d.ts +55 -0
- package/lib/browser/file-read-tracker-impl.d.ts.map +1 -0
- package/lib/browser/file-read-tracker-impl.js +200 -0
- package/lib/browser/file-read-tracker-impl.js.map +1 -0
- package/lib/browser/file-read-tracker-impl.spec.d.ts +2 -0
- package/lib/browser/file-read-tracker-impl.spec.d.ts.map +1 -0
- package/lib/browser/file-read-tracker-impl.spec.js +251 -0
- package/lib/browser/file-read-tracker-impl.spec.js.map +1 -0
- package/lib/common/chat-agents.d.ts +7 -0
- package/lib/common/chat-agents.d.ts.map +1 -1
- package/lib/common/chat-agents.js +28 -0
- package/lib/common/chat-agents.js.map +1 -1
- package/lib/common/chat-agents.spec.js +53 -0
- package/lib/common/chat-agents.spec.js.map +1 -1
- package/lib/common/file-read-tracker.d.ts +12 -0
- package/lib/common/file-read-tracker.d.ts.map +1 -0
- package/lib/common/file-read-tracker.js +20 -0
- package/lib/common/file-read-tracker.js.map +1 -0
- package/lib/common/index.d.ts +1 -0
- package/lib/common/index.d.ts.map +1 -1
- package/lib/common/index.js +1 -0
- package/lib/common/index.js.map +1 -1
- package/package.json +9 -9
- package/src/browser/ai-chat-frontend-module.ts +5 -0
- package/src/browser/change-set-file-element.ts +8 -5
- package/src/browser/file-read-tracker-impl.spec.ts +318 -0
- package/src/browser/file-read-tracker-impl.ts +208 -0
- package/src/common/chat-agents.spec.ts +74 -0
- package/src/common/chat-agents.ts +25 -0
- package/src/common/file-read-tracker.ts +29 -0
- package/src/common/index.ts +1 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"file-read-tracker.d.ts","sourceRoot":"","sources":["../../src/common/file-read-tracker.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAE,GAAG,EAAE,MAAM,aAAa,CAAC;AAElC,eAAO,MAAM,eAAe,eAA4B,CAAC;AAEzD,wHAAwH;AACxH,MAAM,WAAW,eAAe;IAC5B,mIAAmI;IACnI,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACzE,gJAAgJ;IAChJ,OAAO,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACvD,uIAAuI;IACvI,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;CACzD"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// *****************************************************************************
|
|
3
|
+
// Copyright (C) 2026 Ehab Younes.
|
|
4
|
+
//
|
|
5
|
+
// This program and the accompanying materials are made available under the
|
|
6
|
+
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
7
|
+
// http://www.eclipse.org/legal/epl-2.0.
|
|
8
|
+
//
|
|
9
|
+
// This Source Code may also be made available under the following Secondary
|
|
10
|
+
// Licenses when the conditions for such availability set forth in the Eclipse
|
|
11
|
+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
12
|
+
// with the GNU Classpath Exception which is available at
|
|
13
|
+
// https://www.gnu.org/software/classpath/license.html.
|
|
14
|
+
//
|
|
15
|
+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
16
|
+
// *****************************************************************************
|
|
17
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
exports.FileReadTracker = void 0;
|
|
19
|
+
exports.FileReadTracker = Symbol('FileReadTracker');
|
|
20
|
+
//# sourceMappingURL=file-read-tracker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"file-read-tracker.js","sourceRoot":"","sources":["../../src/common/file-read-tracker.ts"],"names":[],"mappings":";AAAA,gFAAgF;AAChF,kCAAkC;AAClC,EAAE;AACF,2EAA2E;AAC3E,mEAAmE;AACnE,wCAAwC;AACxC,EAAE;AACF,4EAA4E;AAC5E,8EAA8E;AAC9E,6EAA6E;AAC7E,yDAAyD;AACzD,uDAAuD;AACvD,EAAE;AACF,gFAAgF;AAChF,gFAAgF;;;AAInE,QAAA,eAAe,GAAG,MAAM,CAAC,iBAAiB,CAAC,CAAC"}
|
package/lib/common/index.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ export * from './custom-chat-agent';
|
|
|
12
12
|
export * from './parsed-chat-request';
|
|
13
13
|
export * from './context-variables';
|
|
14
14
|
export * from './chat-tool-request-service';
|
|
15
|
+
export * from './file-read-tracker';
|
|
15
16
|
export * from './chat-tool-confirmation-timeout';
|
|
16
17
|
export * from './provider-error-formatter';
|
|
17
18
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/common/index.ts"],"names":[],"mappings":"AAeA,cAAc,eAAe,CAAC;AAC9B,cAAc,sBAAsB,CAAC;AACrC,cAAc,qCAAqC,CAAC;AACpD,cAAc,cAAc,CAAC;AAC7B,cAAc,4BAA4B,CAAC;AAC3C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,mBAAmB,CAAC;AAClC,cAAc,uBAAuB,CAAC;AACtC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,sBAAsB,CAAC;AACrC,cAAc,qBAAqB,CAAC;AACpC,cAAc,uBAAuB,CAAC;AACtC,cAAc,qBAAqB,CAAC;AACpC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,kCAAkC,CAAC;AACjD,cAAc,4BAA4B,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/common/index.ts"],"names":[],"mappings":"AAeA,cAAc,eAAe,CAAC;AAC9B,cAAc,sBAAsB,CAAC;AACrC,cAAc,qCAAqC,CAAC;AACpD,cAAc,cAAc,CAAC;AAC7B,cAAc,4BAA4B,CAAC;AAC3C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,mBAAmB,CAAC;AAClC,cAAc,uBAAuB,CAAC;AACtC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,sBAAsB,CAAC;AACrC,cAAc,qBAAqB,CAAC;AACpC,cAAc,uBAAuB,CAAC;AACtC,cAAc,qBAAqB,CAAC;AACpC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,qBAAqB,CAAC;AACpC,cAAc,kCAAkC,CAAC;AACjD,cAAc,4BAA4B,CAAC"}
|
package/lib/common/index.js
CHANGED
|
@@ -30,6 +30,7 @@ tslib_1.__exportStar(require("./custom-chat-agent"), exports);
|
|
|
30
30
|
tslib_1.__exportStar(require("./parsed-chat-request"), exports);
|
|
31
31
|
tslib_1.__exportStar(require("./context-variables"), exports);
|
|
32
32
|
tslib_1.__exportStar(require("./chat-tool-request-service"), exports);
|
|
33
|
+
tslib_1.__exportStar(require("./file-read-tracker"), exports);
|
|
33
34
|
tslib_1.__exportStar(require("./chat-tool-confirmation-timeout"), exports);
|
|
34
35
|
tslib_1.__exportStar(require("./provider-error-formatter"), exports);
|
|
35
36
|
//# sourceMappingURL=index.js.map
|
package/lib/common/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/common/index.ts"],"names":[],"mappings":";;;AAAA,gFAAgF;AAChF,yCAAyC;AACzC,EAAE;AACF,2EAA2E;AAC3E,mEAAmE;AACnE,wCAAwC;AACxC,EAAE;AACF,4EAA4E;AAC5E,8EAA8E;AAC9E,6EAA6E;AAC7E,yDAAyD;AACzD,uDAAuD;AACvD,EAAE;AACF,gFAAgF;AAChF,gFAAgF;AAChF,wDAA8B;AAC9B,+DAAqC;AACrC,8EAAoD;AACpD,uDAA6B;AAC7B,qEAA2C;AAC3C,sEAA4C;AAC5C,4DAAkC;AAClC,gEAAsC;AACtC,yDAA+B;AAC/B,+DAAqC;AACrC,8DAAoC;AACpC,gEAAsC;AACtC,8DAAoC;AACpC,sEAA4C;AAC5C,2EAAiD;AACjD,qEAA2C"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/common/index.ts"],"names":[],"mappings":";;;AAAA,gFAAgF;AAChF,yCAAyC;AACzC,EAAE;AACF,2EAA2E;AAC3E,mEAAmE;AACnE,wCAAwC;AACxC,EAAE;AACF,4EAA4E;AAC5E,8EAA8E;AAC9E,6EAA6E;AAC7E,yDAAyD;AACzD,uDAAuD;AACvD,EAAE;AACF,gFAAgF;AAChF,gFAAgF;AAChF,wDAA8B;AAC9B,+DAAqC;AACrC,8EAAoD;AACpD,uDAA6B;AAC7B,qEAA2C;AAC3C,sEAA4C;AAC5C,4DAAkC;AAClC,gEAAsC;AACtC,yDAA+B;AAC/B,+DAAqC;AACrC,8DAAoC;AACpC,gEAAsC;AACtC,8DAAoC;AACpC,sEAA4C;AAC5C,8DAAoC;AACpC,2EAAiD;AACjD,qEAA2C"}
|
package/package.json
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@theia/ai-chat",
|
|
3
|
-
"version": "1.75.0-next.
|
|
3
|
+
"version": "1.75.0-next.28+ba1005c0bf8",
|
|
4
4
|
"description": "Theia - AI Chat Extension",
|
|
5
5
|
"dependencies": {
|
|
6
|
-
"@theia/ai-core": "1.75.0-next.
|
|
7
|
-
"@theia/core": "1.75.0-next.
|
|
8
|
-
"@theia/editor": "1.75.0-next.
|
|
9
|
-
"@theia/file-search": "1.75.0-next.
|
|
10
|
-
"@theia/filesystem": "1.75.0-next.
|
|
11
|
-
"@theia/monaco": "1.75.0-next.
|
|
6
|
+
"@theia/ai-core": "1.75.0-next.28+ba1005c0bf8",
|
|
7
|
+
"@theia/core": "1.75.0-next.28+ba1005c0bf8",
|
|
8
|
+
"@theia/editor": "1.75.0-next.28+ba1005c0bf8",
|
|
9
|
+
"@theia/file-search": "1.75.0-next.28+ba1005c0bf8",
|
|
10
|
+
"@theia/filesystem": "1.75.0-next.28+ba1005c0bf8",
|
|
11
|
+
"@theia/monaco": "1.75.0-next.28+ba1005c0bf8",
|
|
12
12
|
"@theia/monaco-editor-core": "1.108.201",
|
|
13
|
-
"@theia/workspace": "1.75.0-next.
|
|
13
|
+
"@theia/workspace": "1.75.0-next.28+ba1005c0bf8",
|
|
14
14
|
"js-yaml": "^4.3.1",
|
|
15
15
|
"tslib": "^2.8.1"
|
|
16
16
|
},
|
|
@@ -54,5 +54,5 @@
|
|
|
54
54
|
"nyc": {
|
|
55
55
|
"extends": "../../configs/nyc.json"
|
|
56
56
|
},
|
|
57
|
-
"gitHead": "
|
|
57
|
+
"gitHead": "ba1005c0bf87bd848c6ec51ce1b4e6bb06748e3c"
|
|
58
58
|
}
|
|
@@ -42,6 +42,8 @@ import { CustomAgentFactory } from './custom-agent-factory';
|
|
|
42
42
|
import { ChatToolRequestService } from '../common/chat-tool-request-service';
|
|
43
43
|
import { FrontendChatToolRequestService } from './chat-tool-request-service';
|
|
44
44
|
import { ChangeSetFileService } from './change-set-file-service';
|
|
45
|
+
import { FileReadTracker } from '../common/file-read-tracker';
|
|
46
|
+
import { FileReadTrackerImpl } from './file-read-tracker-impl';
|
|
45
47
|
import { ContextVariableLabelProvider } from './context-variable-label-provider';
|
|
46
48
|
import { ContextFileVariableLabelProvider } from './context-file-variable-label-provider';
|
|
47
49
|
import { FileChatVariableContribution } from './file-chat-variable-contribution';
|
|
@@ -168,6 +170,9 @@ export default new ContainerModule(bind => {
|
|
|
168
170
|
bind(ContextFileVariableLabelProvider).toSelf().inSingletonScope();
|
|
169
171
|
bind(LabelProviderContribution).toService(ContextFileVariableLabelProvider);
|
|
170
172
|
|
|
173
|
+
bind(FileReadTrackerImpl).toSelf().inSingletonScope();
|
|
174
|
+
bind(FileReadTracker).toService(FileReadTrackerImpl);
|
|
175
|
+
|
|
171
176
|
bind(ChangeSetFileService).toSelf().inSingletonScope();
|
|
172
177
|
bind(ChangeSetFileElementFactory).toFactory(ctx => (args: ChangeSetElementArgs) => {
|
|
173
178
|
const container = ctx.container.createChild();
|
|
@@ -34,6 +34,7 @@ import { insertFinalNewline } from '@theia/monaco/lib/browser/monaco-utilities';
|
|
|
34
34
|
import { MonacoEditorModel } from '@theia/monaco/lib/browser/monaco-editor-model';
|
|
35
35
|
import { MonacoWorkspace } from '@theia/monaco/lib/browser/monaco-workspace';
|
|
36
36
|
import { ChangeSetElement } from '../common';
|
|
37
|
+
import { FileReadTracker } from '../common/file-read-tracker';
|
|
37
38
|
import { SerializableChangeSetElement } from '../common/chat-model-serialization';
|
|
38
39
|
import { createChangeSetFileUri } from './change-set-file-resource';
|
|
39
40
|
import { ChangeSetFileService } from './change-set-file-service';
|
|
@@ -106,6 +107,9 @@ export class ChangeSetFileElement implements ChangeSetElement {
|
|
|
106
107
|
@inject(MonacoWorkspace)
|
|
107
108
|
protected readonly monacoWorkspace: MonacoWorkspace;
|
|
108
109
|
|
|
110
|
+
@inject(FileReadTracker)
|
|
111
|
+
protected readonly fileReadTracker: FileReadTracker;
|
|
112
|
+
|
|
109
113
|
protected readonly toDispose = new DisposableCollection();
|
|
110
114
|
protected _state: ChangeSetElementState;
|
|
111
115
|
|
|
@@ -303,13 +307,12 @@ export class ChangeSetFileElement implements ChangeSetElement {
|
|
|
303
307
|
if (this.type === 'delete') {
|
|
304
308
|
await this.changeSetFileService.delete(this.uri);
|
|
305
309
|
this.state = 'applied';
|
|
306
|
-
|
|
307
|
-
|
|
310
|
+
} else {
|
|
311
|
+
await this.applyChangesWithMonaco(contents);
|
|
308
312
|
}
|
|
309
|
-
|
|
310
|
-
// Load Monaco model for the base file URI and apply changes
|
|
311
|
-
await this.applyChangesWithMonaco(contents);
|
|
312
313
|
this.changeSetFileService.closeDiff(this.readOnlyUri);
|
|
314
|
+
// Re-snapshot so the agent's own write is not reported back as external, reading because code actions and formatting on save alter the target state.
|
|
315
|
+
await this.fileReadTracker.recordRead(this.elementProps.chatSessionId, this.uri);
|
|
313
316
|
}
|
|
314
317
|
|
|
315
318
|
async writeChanges(contents?: string): Promise<void> {
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2026 Ehab Younes.
|
|
3
|
+
//
|
|
4
|
+
// This program and the accompanying materials are made available under the
|
|
5
|
+
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
6
|
+
// http://www.eclipse.org/legal/epl-2.0.
|
|
7
|
+
//
|
|
8
|
+
// This Source Code may also be made available under the following Secondary
|
|
9
|
+
// Licenses when the conditions for such availability set forth in the Eclipse
|
|
10
|
+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
11
|
+
// with the GNU Classpath Exception which is available at
|
|
12
|
+
// https://www.gnu.org/software/classpath/license.html.
|
|
13
|
+
//
|
|
14
|
+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
|
+
// *****************************************************************************
|
|
16
|
+
|
|
17
|
+
import { enableJSDOM } from '@theia/core/lib/browser/test/jsdom';
|
|
18
|
+
const disableJSDOM = enableJSDOM();
|
|
19
|
+
import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider';
|
|
20
|
+
FrontendApplicationConfigProvider.set({});
|
|
21
|
+
|
|
22
|
+
import { Emitter, Event, URI } from '@theia/core';
|
|
23
|
+
import { BinaryBuffer } from '@theia/core/lib/common/buffer';
|
|
24
|
+
import { FileService } from '@theia/filesystem/lib/browser/file-service';
|
|
25
|
+
import { FileChangesEvent, FileChangeType, FileOperationError, FileOperationResult, FileStat } from '@theia/filesystem/lib/common/files';
|
|
26
|
+
import { MonacoWorkspace } from '@theia/monaco/lib/browser/monaco-workspace';
|
|
27
|
+
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
|
|
28
|
+
import { expect } from 'chai';
|
|
29
|
+
import { FileReadTrackerImpl } from './file-read-tracker-impl';
|
|
30
|
+
|
|
31
|
+
disableJSDOM();
|
|
32
|
+
|
|
33
|
+
const SESSION = 'session-1';
|
|
34
|
+
const OTHER_SESSION = 'session-2';
|
|
35
|
+
const ROOT = new URI('file:///workspace');
|
|
36
|
+
const FILE = ROOT.resolve('a.ts');
|
|
37
|
+
/** As `resolveRelativePath` expects it back: root name plus path within that root. */
|
|
38
|
+
const FILE_LABEL = 'workspace/a.ts';
|
|
39
|
+
|
|
40
|
+
/** Drives the tracker against in-memory content: `contents` stands in for the files on disk. */
|
|
41
|
+
class TestFileReadTracker extends FileReadTrackerImpl {
|
|
42
|
+
readonly contents = new Map<string, string>([[FILE.toString(), 'original']]);
|
|
43
|
+
/** Makes reading fail as a file too large to compare would, rather than as a missing one. */
|
|
44
|
+
readFails = false;
|
|
45
|
+
/** Runs once inside a read, to change the file while the tracker is still reading it. */
|
|
46
|
+
duringNextRead?: () => void;
|
|
47
|
+
/** The encoding the files are stored in: `read` decodes with it, `readFile` hands back their bytes. */
|
|
48
|
+
encoding: BufferEncoding = 'utf8';
|
|
49
|
+
protected override readonly maxFilesPerSession = 2;
|
|
50
|
+
/** The workspace roots labels are built against. */
|
|
51
|
+
readonly roots = [ROOT];
|
|
52
|
+
protected override readonly workspaceService = {
|
|
53
|
+
tryGetRoots: () => this.roots.map(resource => ({ resource } as FileStat))
|
|
54
|
+
} as WorkspaceService;
|
|
55
|
+
protected override readonly monacoWorkspace = { getTextDocument: () => undefined } as unknown as MonacoWorkspace;
|
|
56
|
+
/** Both decoders, so that a test can tell which one the tracker reads through. */
|
|
57
|
+
protected override readonly fileService = {
|
|
58
|
+
read: async (uri: URI) => ({ value: this.contentOf(uri) }),
|
|
59
|
+
readFile: async (uri: URI) => ({ value: BinaryBuffer.wrap(Buffer.from(this.contentOf(uri), this.encoding)) })
|
|
60
|
+
} as unknown as FileService;
|
|
61
|
+
protected readonly fileChangeEmitter = new Emitter<FileChangesEvent>();
|
|
62
|
+
protected readonly documentChangeEmitter = new Emitter<string>();
|
|
63
|
+
|
|
64
|
+
constructor() {
|
|
65
|
+
super();
|
|
66
|
+
// `@postConstruct` only runs on container resolution, so wire the listeners here.
|
|
67
|
+
this.init();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
protected override get fileChanges(): Event<FileChangesEvent> {
|
|
71
|
+
return this.fileChangeEmitter.event;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
protected override get documentChanges(): Event<string> {
|
|
75
|
+
return this.documentChangeEmitter.event;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Stands in for the file on disk, failing the way the file service would. */
|
|
79
|
+
protected contentOf(uri: URI): string {
|
|
80
|
+
if (this.readFails) {
|
|
81
|
+
throw new FileOperationError(`${uri} is too large`, FileOperationResult.FILE_TOO_LARGE);
|
|
82
|
+
}
|
|
83
|
+
const content = this.contents.get(uri.toString());
|
|
84
|
+
if (content === undefined) {
|
|
85
|
+
throw new FileOperationError(`${uri} does not exist`, FileOperationResult.FILE_NOT_FOUND);
|
|
86
|
+
}
|
|
87
|
+
const during = this.duringNextRead;
|
|
88
|
+
this.duringNextRead = undefined;
|
|
89
|
+
during?.();
|
|
90
|
+
return content;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Records what the agent saw, as `getFileContent` does. */
|
|
94
|
+
agentRead(uri = FILE, sessionId = SESSION): Promise<void> {
|
|
95
|
+
return this.recordRead(sessionId, uri, this.contents.get(uri.toString()));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Changes content behind the agent's back and notifies the tracker, as the workspace would. */
|
|
99
|
+
somebodyElseChanges(content: string, uri = FILE): void {
|
|
100
|
+
this.contents.set(uri.toString(), content);
|
|
101
|
+
this.fileChanged(uri);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
somebodyElseDeletes(uri = FILE): void {
|
|
105
|
+
this.contents.delete(uri.toString());
|
|
106
|
+
this.fileChanged(uri);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
fileChanged(...uris: URI[]): void {
|
|
110
|
+
this.fileChangeEmitter.fire(new FileChangesEvent(uris.map(resource => ({ resource, type: FileChangeType.UPDATED }))));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
documentChanged(uri = FILE): void {
|
|
114
|
+
this.documentChangeEmitter.fire(uri.toString());
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
describe('FileReadTrackerImpl', () => {
|
|
119
|
+
|
|
120
|
+
it('reports a file that changed after it was read', async () => {
|
|
121
|
+
const tracker = new TestFileReadTracker();
|
|
122
|
+
await tracker.agentRead();
|
|
123
|
+
|
|
124
|
+
tracker.somebodyElseChanges('changed');
|
|
125
|
+
|
|
126
|
+
expect(await tracker.getChangedFiles(SESSION)).to.deep.equal([FILE_LABEL]);
|
|
127
|
+
expect(await tracker.isStale(SESSION, FILE)).to.be.true;
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('reports a file whose unsaved document changed', async () => {
|
|
131
|
+
const tracker = new TestFileReadTracker();
|
|
132
|
+
await tracker.agentRead();
|
|
133
|
+
|
|
134
|
+
tracker.contents.set(FILE.toString(), 'the user typed something');
|
|
135
|
+
tracker.documentChanged();
|
|
136
|
+
|
|
137
|
+
expect(await tracker.getChangedFiles(SESSION)).to.deep.equal([FILE_LABEL]);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('keeps reporting a change until the file is read again', async () => {
|
|
141
|
+
const tracker = new TestFileReadTracker();
|
|
142
|
+
await tracker.agentRead();
|
|
143
|
+
tracker.somebodyElseChanges('changed');
|
|
144
|
+
expect(await tracker.getChangedFiles(SESSION)).to.have.lengthOf(1);
|
|
145
|
+
expect(await tracker.getChangedFiles(SESSION)).to.have.lengthOf(1);
|
|
146
|
+
|
|
147
|
+
await tracker.agentRead();
|
|
148
|
+
|
|
149
|
+
expect(await tracker.getChangedFiles(SESSION)).to.be.empty;
|
|
150
|
+
expect(await tracker.isStale(SESSION, FILE)).to.be.false;
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('ignores files that were never read', async () => {
|
|
154
|
+
const tracker = new TestFileReadTracker();
|
|
155
|
+
|
|
156
|
+
tracker.somebodyElseChanges('changed');
|
|
157
|
+
|
|
158
|
+
expect(await tracker.getChangedFiles(SESSION)).to.be.empty;
|
|
159
|
+
expect(await tracker.isStale(SESSION, FILE)).to.be.false;
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('ignores a change that left the content untouched', async () => {
|
|
163
|
+
const tracker = new TestFileReadTracker();
|
|
164
|
+
await tracker.agentRead();
|
|
165
|
+
|
|
166
|
+
// e.g. saving an unmodified document, or a formatter that had nothing to do
|
|
167
|
+
tracker.fileChanged(FILE);
|
|
168
|
+
|
|
169
|
+
expect(await tracker.getChangedFiles(SESSION)).to.be.empty;
|
|
170
|
+
expect(await tracker.isStale(SESSION, FILE)).to.be.false;
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('stops reporting once an edit is undone', async () => {
|
|
174
|
+
const tracker = new TestFileReadTracker();
|
|
175
|
+
await tracker.agentRead();
|
|
176
|
+
tracker.somebodyElseChanges('changed');
|
|
177
|
+
expect(await tracker.getChangedFiles(SESSION)).to.have.lengthOf(1);
|
|
178
|
+
|
|
179
|
+
tracker.somebodyElseChanges('original');
|
|
180
|
+
|
|
181
|
+
expect(await tracker.getChangedFiles(SESSION)).to.be.empty;
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('reports a deleted file once, then forgets it and allows writing it', async () => {
|
|
185
|
+
const tracker = new TestFileReadTracker();
|
|
186
|
+
await tracker.agentRead();
|
|
187
|
+
|
|
188
|
+
tracker.somebodyElseDeletes();
|
|
189
|
+
|
|
190
|
+
expect(await tracker.getChangedFiles(SESSION)).to.deep.equal([FILE_LABEL]);
|
|
191
|
+
expect(await tracker.getChangedFiles(SESSION)).to.be.empty;
|
|
192
|
+
expect(await tracker.isStale(SESSION, FILE)).to.be.false;
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('forgets a file that no longer exists when it is recorded', async () => {
|
|
196
|
+
const tracker = new TestFileReadTracker();
|
|
197
|
+
await tracker.agentRead();
|
|
198
|
+
tracker.somebodyElseDeletes();
|
|
199
|
+
|
|
200
|
+
// as the change set element does after applying a deletion
|
|
201
|
+
await tracker.recordRead(SESSION, FILE);
|
|
202
|
+
|
|
203
|
+
expect(await tracker.getChangedFiles(SESSION)).to.be.empty;
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('keeps reporting a file it cannot read, and does not allow overwriting it', async () => {
|
|
207
|
+
const tracker = new TestFileReadTracker();
|
|
208
|
+
await tracker.agentRead();
|
|
209
|
+
tracker.fileChanged(FILE);
|
|
210
|
+
tracker.readFails = true;
|
|
211
|
+
|
|
212
|
+
expect(await tracker.getChangedFiles(SESSION)).to.deep.equal([FILE_LABEL]);
|
|
213
|
+
expect(await tracker.getChangedFiles(SESSION)).to.deep.equal([FILE_LABEL]);
|
|
214
|
+
expect(await tracker.isStale(SESSION, FILE)).to.be.true;
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('reports a change made while the read recording it was still in flight', async () => {
|
|
218
|
+
const tracker = new TestFileReadTracker();
|
|
219
|
+
tracker.duringNextRead = () => {
|
|
220
|
+
tracker.contents.set(FILE.toString(), 'the user typed something');
|
|
221
|
+
tracker.documentChanged();
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
// no content handed in, so the tracker reads it itself
|
|
225
|
+
await tracker.recordRead(SESSION, FILE);
|
|
226
|
+
|
|
227
|
+
expect(await tracker.getChangedFiles(SESSION)).to.deep.equal([FILE_LABEL]);
|
|
228
|
+
expect(await tracker.isStale(SESSION, FILE)).to.be.true;
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('records nothing when the content cannot be read', async () => {
|
|
232
|
+
const tracker = new TestFileReadTracker();
|
|
233
|
+
tracker.readFails = true;
|
|
234
|
+
|
|
235
|
+
await tracker.recordRead(SESSION, FILE);
|
|
236
|
+
|
|
237
|
+
expect(await tracker.getChangedFiles(SESSION)).to.be.empty;
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('treats a write by another session as an external change', async () => {
|
|
241
|
+
const tracker = new TestFileReadTracker();
|
|
242
|
+
await tracker.agentRead();
|
|
243
|
+
await tracker.agentRead(FILE, OTHER_SESSION);
|
|
244
|
+
|
|
245
|
+
tracker.somebodyElseChanges('written by the other session');
|
|
246
|
+
await tracker.recordRead(OTHER_SESSION, FILE);
|
|
247
|
+
|
|
248
|
+
expect(await tracker.getChangedFiles(OTHER_SESSION)).to.be.empty;
|
|
249
|
+
expect(await tracker.getChangedFiles(SESSION)).to.deep.equal([FILE_LABEL]);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('compares a file the read tools decode as UTF-16, not as UTF-8', async () => {
|
|
253
|
+
const tracker = new TestFileReadTracker();
|
|
254
|
+
tracker.encoding = 'utf16le'; // as a Windows-written `.ps1` is stored
|
|
255
|
+
await tracker.agentRead();
|
|
256
|
+
|
|
257
|
+
tracker.fileChanged(FILE);
|
|
258
|
+
|
|
259
|
+
expect(await tracker.getChangedFiles(SESSION)).to.be.empty;
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
describe('naming changed files', () => {
|
|
263
|
+
|
|
264
|
+
/** How the notice names `file` after it changed, in a workspace with the given extra roots. */
|
|
265
|
+
async function nameOf(file: URI, ...extraRoots: URI[]): Promise<string[]> {
|
|
266
|
+
const tracker = new TestFileReadTracker();
|
|
267
|
+
tracker.roots.push(...extraRoots);
|
|
268
|
+
tracker.contents.set(file.toString(), 'original');
|
|
269
|
+
await tracker.agentRead(file);
|
|
270
|
+
tracker.somebodyElseChanges('changed', file);
|
|
271
|
+
return tracker.getChangedFiles(SESSION);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
it('names a file by the root it is in', async () => {
|
|
275
|
+
expect(await nameOf(new URI('file:///elsewhere/lib/b.ts'), new URI('file:///elsewhere/lib'))).to.deep.equal(['lib/b.ts']);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('names a file by its root when that root is listed twice', async () => {
|
|
279
|
+
expect(await nameOf(FILE, ROOT)).to.deep.equal([FILE_LABEL]);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it('names a file by uri when two roots share the name it would use', async () => {
|
|
283
|
+
expect(await nameOf(FILE, new URI('file:///elsewhere/workspace'))).to.deep.equal([FILE.toString()]);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it('names a file by uri when it is outside every root', async () => {
|
|
287
|
+
expect(await nameOf(new URI('file:///etc/hosts'))).to.deep.equal(['file:///etc/hosts']);
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it('evicts the least recently recorded file beyond the per-session limit', async () => {
|
|
292
|
+
const tracker = new TestFileReadTracker();
|
|
293
|
+
const files = ['a', 'b', 'c'].map(name => new URI(`file:///workspace/${name}.ts`));
|
|
294
|
+
for (const uri of files) {
|
|
295
|
+
tracker.contents.set(uri.toString(), 'original');
|
|
296
|
+
await tracker.agentRead(uri);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
files.forEach(uri => tracker.somebodyElseChanges('changed', uri));
|
|
300
|
+
|
|
301
|
+
expect(await tracker.getChangedFiles(SESSION)).to.deep.equal(['workspace/b.ts', 'workspace/c.ts']);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it('keeps a re-read file tracked, evicting the one untouched for longest', async () => {
|
|
305
|
+
const tracker = new TestFileReadTracker();
|
|
306
|
+
const [a, b, c] = ['a', 'b', 'c'].map(name => new URI(`file:///workspace/${name}.ts`));
|
|
307
|
+
[a, b, c].forEach(uri => tracker.contents.set(uri.toString(), 'original'));
|
|
308
|
+
await tracker.agentRead(a);
|
|
309
|
+
await tracker.agentRead(b);
|
|
310
|
+
await tracker.agentRead(a);
|
|
311
|
+
await tracker.agentRead(c);
|
|
312
|
+
|
|
313
|
+
tracker.somebodyElseChanges('changed', a);
|
|
314
|
+
tracker.somebodyElseChanges('changed', b);
|
|
315
|
+
|
|
316
|
+
expect(await tracker.getChangedFiles(SESSION)).to.deep.equal(['workspace/a.ts']);
|
|
317
|
+
});
|
|
318
|
+
});
|