@theia/ai-ide 1.72.1 → 1.72.3
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-configuration/ai-configuration-view-contribution.d.ts +1 -0
- package/lib/browser/ai-configuration/ai-configuration-view-contribution.d.ts.map +1 -1
- package/lib/browser/ai-configuration/ai-configuration-view-contribution.js +15 -2
- package/lib/browser/ai-configuration/ai-configuration-view-contribution.js.map +1 -1
- package/lib/browser/ai-configuration/tools-configuration-widget.d.ts +3 -0
- package/lib/browser/ai-configuration/tools-configuration-widget.d.ts.map +1 -1
- package/lib/browser/ai-configuration/tools-configuration-widget.js +61 -4
- package/lib/browser/ai-configuration/tools-configuration-widget.js.map +1 -1
- package/lib/browser/ide-chat-welcome-message-provider.d.ts +11 -0
- package/lib/browser/ide-chat-welcome-message-provider.d.ts.map +1 -1
- package/lib/browser/ide-chat-welcome-message-provider.js +31 -1
- package/lib/browser/ide-chat-welcome-message-provider.js.map +1 -1
- package/lib/browser/todo-tool-renderer.d.ts +7 -1
- package/lib/browser/todo-tool-renderer.d.ts.map +1 -1
- package/lib/browser/todo-tool-renderer.js +25 -1
- package/lib/browser/todo-tool-renderer.js.map +1 -1
- package/lib/browser/user-interaction-tool-renderer.d.ts +7 -1
- package/lib/browser/user-interaction-tool-renderer.d.ts.map +1 -1
- package/lib/browser/user-interaction-tool-renderer.js +25 -1
- package/lib/browser/user-interaction-tool-renderer.js.map +1 -1
- package/lib/browser/workspace-functions.d.ts +27 -3
- package/lib/browser/workspace-functions.d.ts.map +1 -1
- package/lib/browser/workspace-functions.js +123 -140
- package/lib/browser/workspace-functions.js.map +1 -1
- package/lib/browser/workspace-functions.spec.js +323 -24
- package/lib/browser/workspace-functions.spec.js.map +1 -1
- package/package.json +23 -22
- package/src/browser/ai-configuration/ai-configuration-view-contribution.ts +15 -1
- package/src/browser/ai-configuration/tools-configuration-widget.tsx +106 -17
- package/src/browser/ide-chat-welcome-message-provider.tsx +43 -1
- package/src/browser/style/index.css +16 -0
- package/src/browser/todo-tool-renderer.tsx +30 -3
- package/src/browser/user-interaction-tool-renderer.tsx +30 -3
- package/src/browser/workspace-functions.spec.ts +385 -25
- package/src/browser/workspace-functions.ts +128 -195
|
@@ -34,12 +34,49 @@ import { TrustAwarePreferenceReader } from '@theia/ai-core/lib/browser/trust-awa
|
|
|
34
34
|
import { Container } from '@theia/core/shared/inversify';
|
|
35
35
|
import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
|
|
36
36
|
import { FileService } from '@theia/filesystem/lib/browser/file-service';
|
|
37
|
-
import { FileOperationError, FileOperationResult } from '@theia/filesystem/lib/common/files';
|
|
37
|
+
import { FileOperationError, FileOperationResult, FileStat } from '@theia/filesystem/lib/common/files';
|
|
38
38
|
import { URI } from '@theia/core/lib/common/uri';
|
|
39
39
|
import { WorkspaceService } from '@theia/workspace/lib/browser';
|
|
40
40
|
import { ProblemManager } from '@theia/markers/lib/browser';
|
|
41
41
|
import { MonacoTextModelService } from '@theia/monaco/lib/browser/monaco-text-model-service';
|
|
42
42
|
import { MonacoWorkspace } from '@theia/monaco/lib/browser/monaco-workspace';
|
|
43
|
+
import { FileSearchService } from '@theia/file-search/lib/common/file-search-service';
|
|
44
|
+
import { Minimatch } from 'minimatch';
|
|
45
|
+
|
|
46
|
+
const makeFileSearchService = (
|
|
47
|
+
impl?: (searchPattern: string, options: FileSearchService.Options) => Promise<string[]>
|
|
48
|
+
): FileSearchService & { calls: Array<{ searchPattern: string; options: FileSearchService.Options }> } => {
|
|
49
|
+
const calls: Array<{ searchPattern: string; options: FileSearchService.Options }> = [];
|
|
50
|
+
return {
|
|
51
|
+
calls,
|
|
52
|
+
find: async (searchPattern: string, options: FileSearchService.Options) => {
|
|
53
|
+
calls.push({ searchPattern, options });
|
|
54
|
+
return impl ? impl(searchPattern, options) : [];
|
|
55
|
+
}
|
|
56
|
+
} as unknown as FileSearchService & { calls: Array<{ searchPattern: string; options: FileSearchService.Options }> };
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* A FileSearchService stand-in that mimics ripgrep: given a flat map of file URIs per
|
|
61
|
+
* root, it returns the files under the requested root that match the include globs and
|
|
62
|
+
* are not removed by the exclude globs (matched against each file's root-relative path).
|
|
63
|
+
*/
|
|
64
|
+
const makeRipgrepLikeSearchService = (filesByRoot: Record<string, string[]>) =>
|
|
65
|
+
makeFileSearchService(async (_searchPattern, options) => {
|
|
66
|
+
const root = options.rootUris?.[0];
|
|
67
|
+
if (!root) {
|
|
68
|
+
return [];
|
|
69
|
+
}
|
|
70
|
+
const rootUri = new URI(root);
|
|
71
|
+
const includes = (options.includePatterns ?? []).map(pattern => new Minimatch(pattern, { dot: true }));
|
|
72
|
+
const excludes = (options.excludePatterns ?? []).map(pattern => new Minimatch(pattern, { dot: true }));
|
|
73
|
+
return (filesByRoot[root] ?? []).filter(file => {
|
|
74
|
+
const relativePath = rootUri.relative(new URI(file))?.toString() ?? '';
|
|
75
|
+
const included = includes.length === 0 || includes.some(matcher => matcher.match(relativePath));
|
|
76
|
+
const excluded = excludes.some(matcher => matcher.match(relativePath));
|
|
77
|
+
return included && !excluded;
|
|
78
|
+
});
|
|
79
|
+
});
|
|
43
80
|
|
|
44
81
|
const makeTrustAwareReader = (overrides: { [pref: string]: unknown } = {}): TrustAwarePreferenceReader => ({
|
|
45
82
|
get: <T>(name: string, fallback?: T) => (name in overrides ? overrides[name] as T : fallback),
|
|
@@ -136,6 +173,7 @@ describe('Workspace Functions Cancellation Tests', () => {
|
|
|
136
173
|
container.bind(MonacoTextModelService).toConstantValue(mockMonacoTextModelService);
|
|
137
174
|
container.bind(TrustAwarePreferenceReader).toConstantValue(makeTrustAwareReader());
|
|
138
175
|
container.bind(EnvVariablesServer).toConstantValue(makeEnvVariablesServer());
|
|
176
|
+
container.bind(FileSearchService).toConstantValue(makeFileSearchService());
|
|
139
177
|
container.bind(WorkspaceFunctionScope).toSelf();
|
|
140
178
|
container.bind(GetWorkspaceDirectoryStructure).toSelf();
|
|
141
179
|
container.bind(FileContentFunction).toSelf();
|
|
@@ -740,6 +778,7 @@ describe('FindFilesByPattern.getArgumentsShortLabel', () => {
|
|
|
740
778
|
container.bind(PreferenceService).toConstantValue(mockPreferenceService);
|
|
741
779
|
container.bind(TrustAwarePreferenceReader).toConstantValue(makeTrustAwareReader());
|
|
742
780
|
container.bind(EnvVariablesServer).toConstantValue(makeEnvVariablesServer());
|
|
781
|
+
container.bind(FileSearchService).toConstantValue(makeFileSearchService());
|
|
743
782
|
container.bind(WorkspaceFunctionScope).toSelf();
|
|
744
783
|
container.bind(FindFilesByPattern).toSelf();
|
|
745
784
|
|
|
@@ -769,6 +808,345 @@ describe('FindFilesByPattern.getArgumentsShortLabel', () => {
|
|
|
769
808
|
});
|
|
770
809
|
});
|
|
771
810
|
|
|
811
|
+
describe('FindFilesByPattern.findFiles', () => {
|
|
812
|
+
let container: Container;
|
|
813
|
+
let findFilesByPattern: FindFilesByPattern;
|
|
814
|
+
let fileSearchService: ReturnType<typeof makeFileSearchService>;
|
|
815
|
+
let searchResults: string[];
|
|
816
|
+
let roots: Array<{ resource: URI }>;
|
|
817
|
+
let prefs: { [key: string]: unknown };
|
|
818
|
+
let allowedExternal: string[];
|
|
819
|
+
|
|
820
|
+
let disableJSDOMInner: () => void;
|
|
821
|
+
before(() => { disableJSDOMInner = enableJSDOM(); });
|
|
822
|
+
after(() => { disableJSDOMInner(); });
|
|
823
|
+
|
|
824
|
+
beforeEach(() => {
|
|
825
|
+
container = new Container();
|
|
826
|
+
searchResults = [];
|
|
827
|
+
roots = [{ resource: new URI('file:///workspace') }];
|
|
828
|
+
prefs = {
|
|
829
|
+
'ai-features.workspaceFunctions.considerGitIgnore': true,
|
|
830
|
+
'ai-features.workspaceFunctions.userExcludes': ['node_modules', 'lib']
|
|
831
|
+
};
|
|
832
|
+
allowedExternal = [];
|
|
833
|
+
|
|
834
|
+
const mockWorkspaceService = {
|
|
835
|
+
roots: Promise.resolve(roots),
|
|
836
|
+
tryGetRoots: () => roots,
|
|
837
|
+
onWorkspaceChanged: () => ({ dispose: () => { } })
|
|
838
|
+
} as unknown as WorkspaceService;
|
|
839
|
+
|
|
840
|
+
const mockFileService = {
|
|
841
|
+
exists: async () => true,
|
|
842
|
+
resolve: async (uri: URI) => ({ isDirectory: true, children: [], resource: uri }),
|
|
843
|
+
read: async () => ({ value: { toString: () => '' } })
|
|
844
|
+
} as unknown as FileService;
|
|
845
|
+
|
|
846
|
+
const mockPreferenceService = {
|
|
847
|
+
get: <T>(path: string, defaultValue: T) => (path in prefs ? prefs[path] as T : defaultValue)
|
|
848
|
+
};
|
|
849
|
+
|
|
850
|
+
const trustAwareReader = {
|
|
851
|
+
get: <T>(name: string, fallback?: T) =>
|
|
852
|
+
(name === 'ai-features.workspaceFunctions.allowedExternalPaths' ? (allowedExternal as unknown as T) : fallback),
|
|
853
|
+
ready: Promise.resolve(),
|
|
854
|
+
onDidChangeTrust: () => ({ dispose: () => { /* noop */ } })
|
|
855
|
+
} as unknown as TrustAwarePreferenceReader;
|
|
856
|
+
|
|
857
|
+
fileSearchService = makeFileSearchService(async () => searchResults);
|
|
858
|
+
|
|
859
|
+
container.bind(WorkspaceService).toConstantValue(mockWorkspaceService);
|
|
860
|
+
container.bind(FileService).toConstantValue(mockFileService);
|
|
861
|
+
container.bind(PreferenceService).toConstantValue(mockPreferenceService);
|
|
862
|
+
container.bind(TrustAwarePreferenceReader).toConstantValue(trustAwareReader);
|
|
863
|
+
container.bind(EnvVariablesServer).toConstantValue(makeEnvVariablesServer());
|
|
864
|
+
container.bind(FileSearchService).toConstantValue(fileSearchService);
|
|
865
|
+
container.bind(WorkspaceFunctionScope).toSelf();
|
|
866
|
+
container.bind(FindFilesByPattern).toSelf();
|
|
867
|
+
|
|
868
|
+
findFilesByPattern = container.get(FindFilesByPattern);
|
|
869
|
+
});
|
|
870
|
+
|
|
871
|
+
const call = (args: object, ctx?: ToolInvocationContext) =>
|
|
872
|
+
findFilesByPattern.getTool().handler(JSON.stringify(args), ctx) as Promise<string>;
|
|
873
|
+
|
|
874
|
+
it('returns root-prefixed workspace-relative paths', async () => {
|
|
875
|
+
searchResults = ['file:///workspace/src/a.ts', 'file:///workspace/src/b.ts'];
|
|
876
|
+
const result = JSON.parse(await call({ pattern: '**/*.ts' }));
|
|
877
|
+
expect(result.files).to.deep.equal(['workspace/src/a.ts', 'workspace/src/b.ts']);
|
|
878
|
+
});
|
|
879
|
+
|
|
880
|
+
it('passes the glob, user excludes, gitignore flag and root to the search service', async () => {
|
|
881
|
+
await call({ pattern: '**/*.ts', exclude: ['**/*.spec.ts'] });
|
|
882
|
+
expect(fileSearchService.calls).to.have.length(1);
|
|
883
|
+
const options = fileSearchService.calls[0].options;
|
|
884
|
+
expect(options.includePatterns).to.deep.equal(['**/*.ts']);
|
|
885
|
+
expect(options.excludePatterns).to.deep.equal(['node_modules', 'lib', '**/*.spec.ts']);
|
|
886
|
+
expect(options.useGitIgnore).to.equal(true);
|
|
887
|
+
expect(options.rootUris).to.deep.equal(['file:///workspace']);
|
|
888
|
+
expect(options.fuzzyMatch).to.equal(false);
|
|
889
|
+
});
|
|
890
|
+
|
|
891
|
+
it('does not use gitignore when the preference is disabled', async () => {
|
|
892
|
+
prefs['ai-features.workspaceFunctions.considerGitIgnore'] = false;
|
|
893
|
+
await call({ pattern: '**/*.ts' });
|
|
894
|
+
expect(fileSearchService.calls[0].options.useGitIgnore).to.equal(false);
|
|
895
|
+
});
|
|
896
|
+
|
|
897
|
+
it('reports an error when no workspace is open', async () => {
|
|
898
|
+
roots.length = 0;
|
|
899
|
+
const result = JSON.parse(await call({ pattern: '**/*.ts' }));
|
|
900
|
+
expect(result.error).to.equal('No workspace has been opened yet');
|
|
901
|
+
});
|
|
902
|
+
|
|
903
|
+
it('respects the cancellation token', async () => {
|
|
904
|
+
const cts = new CancellationTokenSource();
|
|
905
|
+
cts.cancel();
|
|
906
|
+
const result = JSON.parse(await call({ pattern: '**/*.ts' }, { cancellationToken: cts.token }));
|
|
907
|
+
expect(result.error).to.equal('Operation cancelled by user');
|
|
908
|
+
});
|
|
909
|
+
|
|
910
|
+
it('truncates to 200 results and flags truncation', async () => {
|
|
911
|
+
searchResults = Array.from({ length: 250 }, (_, i) => `file:///workspace/f${i}.ts`);
|
|
912
|
+
const result = JSON.parse(await call({ pattern: '**/*.ts' }));
|
|
913
|
+
expect(result.files).to.have.length(200);
|
|
914
|
+
expect(result.truncated).to.equal(true);
|
|
915
|
+
});
|
|
916
|
+
|
|
917
|
+
it('returns absolute paths for an allow-listed external searchRoot', async () => {
|
|
918
|
+
allowedExternal = ['/external/data'];
|
|
919
|
+
searchResults = ['file:///external/data/x.ts'];
|
|
920
|
+
const result = JSON.parse(await call({ pattern: '**/*.ts', searchRoot: '/external/data' }));
|
|
921
|
+
expect(result.files).to.deep.equal(['/external/data/x.ts']);
|
|
922
|
+
expect(fileSearchService.calls[0].options.rootUris).to.deep.equal(['file:///external/data']);
|
|
923
|
+
});
|
|
924
|
+
|
|
925
|
+
it('does not apply gitignore to external roots (it is scoped to workspace roots)', async () => {
|
|
926
|
+
prefs['ai-features.workspaceFunctions.considerGitIgnore'] = true;
|
|
927
|
+
allowedExternal = ['/external/data'];
|
|
928
|
+
await call({ pattern: '**/*.ts', searchRoot: '/external/data' });
|
|
929
|
+
const options = fileSearchService.calls[0].options;
|
|
930
|
+
expect(options.useGitIgnore).to.equal(false);
|
|
931
|
+
expect(options.excludePatterns).to.include('.git');
|
|
932
|
+
});
|
|
933
|
+
|
|
934
|
+
it('still applies gitignore to workspace roots', async () => {
|
|
935
|
+
prefs['ai-features.workspaceFunctions.considerGitIgnore'] = true;
|
|
936
|
+
await call({ pattern: '**/*.ts' });
|
|
937
|
+
expect(fileSearchService.calls[0].options.useGitIgnore).to.equal(true);
|
|
938
|
+
});
|
|
939
|
+
|
|
940
|
+
it('matches an allow-list entry with a trailing slash against a searchRoot without one', async () => {
|
|
941
|
+
allowedExternal = ['/external/data/']; // trailing slash, as a user/file-picker may enter it
|
|
942
|
+
searchResults = ['file:///external/data/x.ts'];
|
|
943
|
+
const result = JSON.parse(await call({ pattern: '**/*.ts', searchRoot: '/external/data' }));
|
|
944
|
+
expect(result.error).to.be.undefined;
|
|
945
|
+
expect(result.files).to.deep.equal(['/external/data/x.ts']);
|
|
946
|
+
});
|
|
947
|
+
|
|
948
|
+
it('matches an allow-list entry without a trailing slash against a searchRoot that has one', async () => {
|
|
949
|
+
allowedExternal = ['/external/data'];
|
|
950
|
+
searchResults = ['file:///external/data/x.ts'];
|
|
951
|
+
const result = JSON.parse(await call({ pattern: '**/*.ts', searchRoot: '/external/data/' }));
|
|
952
|
+
expect(result.error).to.be.undefined;
|
|
953
|
+
expect(result.files).to.deep.equal(['/external/data/x.ts']);
|
|
954
|
+
});
|
|
955
|
+
});
|
|
956
|
+
|
|
957
|
+
describe('WorkspaceFunctionScope gitignore caching', () => {
|
|
958
|
+
let container: Container;
|
|
959
|
+
let scope: WorkspaceFunctionScope;
|
|
960
|
+
let resolveCount: number;
|
|
961
|
+
let gitignoreReadCount: number;
|
|
962
|
+
|
|
963
|
+
let disableJSDOMInner: () => void;
|
|
964
|
+
before(() => { disableJSDOMInner = enableJSDOM(); });
|
|
965
|
+
after(() => { disableJSDOMInner(); });
|
|
966
|
+
|
|
967
|
+
beforeEach(() => {
|
|
968
|
+
container = new Container();
|
|
969
|
+
resolveCount = 0;
|
|
970
|
+
gitignoreReadCount = 0;
|
|
971
|
+
|
|
972
|
+
const mockWorkspaceService = {
|
|
973
|
+
roots: Promise.resolve([{ resource: new URI('file:///workspace') }]),
|
|
974
|
+
tryGetRoots: () => [{ resource: new URI('file:///workspace') }],
|
|
975
|
+
onWorkspaceChanged: () => ({ dispose: () => { } })
|
|
976
|
+
} as unknown as WorkspaceService;
|
|
977
|
+
|
|
978
|
+
const mockFileService = {
|
|
979
|
+
resolve: async (uri: URI) => { resolveCount++; return { isDirectory: true, resource: uri }; },
|
|
980
|
+
read: async (uri: URI) => {
|
|
981
|
+
if (uri.path.base === '.gitignore') {
|
|
982
|
+
gitignoreReadCount++;
|
|
983
|
+
return { value: 'dist/\n' };
|
|
984
|
+
}
|
|
985
|
+
throw new Error('not found');
|
|
986
|
+
},
|
|
987
|
+
watch: () => ({ dispose: () => { } }),
|
|
988
|
+
onDidFilesChange: () => ({ dispose: () => { } })
|
|
989
|
+
} as unknown as FileService;
|
|
990
|
+
|
|
991
|
+
const mockPreferenceService = {
|
|
992
|
+
get: <T>(path: string, defaultValue: T) =>
|
|
993
|
+
(path === 'ai-features.workspaceFunctions.considerGitIgnore' ? (true as unknown as T) : defaultValue)
|
|
994
|
+
};
|
|
995
|
+
|
|
996
|
+
container.bind(WorkspaceService).toConstantValue(mockWorkspaceService);
|
|
997
|
+
container.bind(FileService).toConstantValue(mockFileService);
|
|
998
|
+
container.bind(PreferenceService).toConstantValue(mockPreferenceService);
|
|
999
|
+
container.bind(TrustAwarePreferenceReader).toConstantValue(makeTrustAwareReader());
|
|
1000
|
+
container.bind(EnvVariablesServer).toConstantValue(makeEnvVariablesServer());
|
|
1001
|
+
container.bind(WorkspaceFunctionScope).toSelf();
|
|
1002
|
+
|
|
1003
|
+
scope = container.get(WorkspaceFunctionScope);
|
|
1004
|
+
});
|
|
1005
|
+
|
|
1006
|
+
const stat = (path: string, isDirectory = false) => ({
|
|
1007
|
+
resource: new URI(path),
|
|
1008
|
+
isDirectory,
|
|
1009
|
+
path: { base: path.split('/').pop() }
|
|
1010
|
+
}) as unknown as FileStat;
|
|
1011
|
+
|
|
1012
|
+
it('still excludes gitignored paths and keeps others', async () => {
|
|
1013
|
+
expect(await scope.shouldExclude(stat('file:///workspace/dist/a.js'))).to.be.true;
|
|
1014
|
+
expect(await scope.shouldExclude(stat('file:///workspace/src/b.ts'))).to.be.false;
|
|
1015
|
+
});
|
|
1016
|
+
|
|
1017
|
+
it('reads .gitignore at most once and issues no per-check resolve RPC', async () => {
|
|
1018
|
+
for (let i = 0; i < 25; i++) {
|
|
1019
|
+
await scope.shouldExclude(stat(`file:///workspace/src/file${i}.ts`));
|
|
1020
|
+
}
|
|
1021
|
+
expect(gitignoreReadCount).to.equal(1);
|
|
1022
|
+
expect(resolveCount).to.equal(0);
|
|
1023
|
+
});
|
|
1024
|
+
});
|
|
1025
|
+
|
|
1026
|
+
describe('GetWorkspaceFileList resolves the target directory once', () => {
|
|
1027
|
+
let container: Container;
|
|
1028
|
+
let tool: GetWorkspaceFileList;
|
|
1029
|
+
let resolveCount: number;
|
|
1030
|
+
|
|
1031
|
+
let disableJSDOMInner: () => void;
|
|
1032
|
+
before(() => { disableJSDOMInner = enableJSDOM(); });
|
|
1033
|
+
after(() => { disableJSDOMInner(); });
|
|
1034
|
+
|
|
1035
|
+
beforeEach(() => {
|
|
1036
|
+
container = new Container();
|
|
1037
|
+
resolveCount = 0;
|
|
1038
|
+
|
|
1039
|
+
const mockWorkspaceService = {
|
|
1040
|
+
roots: Promise.resolve([{ resource: new URI('file:///workspace') }]),
|
|
1041
|
+
tryGetRoots: () => [{ resource: new URI('file:///workspace') }],
|
|
1042
|
+
onWorkspaceChanged: () => ({ dispose: () => { } })
|
|
1043
|
+
} as unknown as WorkspaceService;
|
|
1044
|
+
|
|
1045
|
+
const mockFileService = {
|
|
1046
|
+
exists: async () => true,
|
|
1047
|
+
resolve: async (uri: URI) => {
|
|
1048
|
+
resolveCount++;
|
|
1049
|
+
return {
|
|
1050
|
+
isDirectory: true,
|
|
1051
|
+
resource: uri,
|
|
1052
|
+
children: [
|
|
1053
|
+
{ isDirectory: false, resource: new URI('file:///workspace/sub/a.ts'), path: { base: 'a.ts' } },
|
|
1054
|
+
{ isDirectory: true, resource: new URI('file:///workspace/sub/dir'), path: { base: 'dir' } }
|
|
1055
|
+
]
|
|
1056
|
+
};
|
|
1057
|
+
},
|
|
1058
|
+
read: async () => ({ value: { toString: () => '' } }),
|
|
1059
|
+
watch: () => ({ dispose: () => { } }),
|
|
1060
|
+
onDidFilesChange: () => ({ dispose: () => { } })
|
|
1061
|
+
} as unknown as FileService;
|
|
1062
|
+
|
|
1063
|
+
const mockPreferenceService = { get: <T>(_path: string, def: T) => def };
|
|
1064
|
+
|
|
1065
|
+
container.bind(WorkspaceService).toConstantValue(mockWorkspaceService);
|
|
1066
|
+
container.bind(FileService).toConstantValue(mockFileService);
|
|
1067
|
+
container.bind(PreferenceService).toConstantValue(mockPreferenceService);
|
|
1068
|
+
container.bind(TrustAwarePreferenceReader).toConstantValue(makeTrustAwareReader());
|
|
1069
|
+
container.bind(EnvVariablesServer).toConstantValue(makeEnvVariablesServer());
|
|
1070
|
+
container.bind(WorkspaceFunctionScope).toSelf();
|
|
1071
|
+
container.bind(GetWorkspaceFileList).toSelf();
|
|
1072
|
+
|
|
1073
|
+
tool = container.get(GetWorkspaceFileList);
|
|
1074
|
+
});
|
|
1075
|
+
|
|
1076
|
+
it('lists one level and resolves the directory only once', async () => {
|
|
1077
|
+
const result = JSON.parse(await tool.getTool().handler(JSON.stringify({ path: 'workspace/sub' }), undefined) as string);
|
|
1078
|
+
expect(result).to.deep.equal({ 'a.ts': 'file', 'dir': 'directory' });
|
|
1079
|
+
expect(resolveCount).to.equal(1);
|
|
1080
|
+
});
|
|
1081
|
+
});
|
|
1082
|
+
|
|
1083
|
+
describe('GetWorkspaceDirectoryStructure preserves empty folders', () => {
|
|
1084
|
+
let container: Container;
|
|
1085
|
+
let tool: GetWorkspaceDirectoryStructure;
|
|
1086
|
+
|
|
1087
|
+
const TREE: Record<string, string[]> = {
|
|
1088
|
+
'file:///workspace': ['file:///workspace/src', 'file:///workspace/empty', 'file:///workspace/node_modules'],
|
|
1089
|
+
'file:///workspace/src': ['file:///workspace/src/nested'],
|
|
1090
|
+
'file:///workspace/src/nested': [],
|
|
1091
|
+
'file:///workspace/empty': [],
|
|
1092
|
+
'file:///workspace/node_modules': ['file:///workspace/node_modules/pkg']
|
|
1093
|
+
};
|
|
1094
|
+
|
|
1095
|
+
let disableJSDOMInner: () => void;
|
|
1096
|
+
before(() => { disableJSDOMInner = enableJSDOM(); });
|
|
1097
|
+
after(() => { disableJSDOMInner(); });
|
|
1098
|
+
|
|
1099
|
+
beforeEach(() => {
|
|
1100
|
+
container = new Container();
|
|
1101
|
+
|
|
1102
|
+
const mockWorkspaceService = {
|
|
1103
|
+
roots: Promise.resolve([{ resource: new URI('file:///workspace') }]),
|
|
1104
|
+
tryGetRoots: () => [{ resource: new URI('file:///workspace') }],
|
|
1105
|
+
onWorkspaceChanged: () => ({ dispose: () => { } })
|
|
1106
|
+
} as unknown as WorkspaceService;
|
|
1107
|
+
|
|
1108
|
+
const mockFileService = {
|
|
1109
|
+
resolve: async (uri: URI) => ({
|
|
1110
|
+
isDirectory: true,
|
|
1111
|
+
resource: uri,
|
|
1112
|
+
children: (TREE[uri.toString()] ?? []).map(child => ({
|
|
1113
|
+
isDirectory: true,
|
|
1114
|
+
resource: new URI(child),
|
|
1115
|
+
path: { base: child.split('/').pop() }
|
|
1116
|
+
}))
|
|
1117
|
+
}),
|
|
1118
|
+
read: async () => ({ value: { toString: () => '' } }),
|
|
1119
|
+
watch: () => ({ dispose: () => { } }),
|
|
1120
|
+
onDidFilesChange: () => ({ dispose: () => { } })
|
|
1121
|
+
} as unknown as FileService;
|
|
1122
|
+
|
|
1123
|
+
const mockPreferenceService = {
|
|
1124
|
+
get: <T>(path: string, def: T) =>
|
|
1125
|
+
(path === 'ai-features.workspaceFunctions.userExcludes' ? (['node_modules'] as unknown as T) : def)
|
|
1126
|
+
};
|
|
1127
|
+
|
|
1128
|
+
container.bind(WorkspaceService).toConstantValue(mockWorkspaceService);
|
|
1129
|
+
container.bind(FileService).toConstantValue(mockFileService);
|
|
1130
|
+
container.bind(PreferenceService).toConstantValue(mockPreferenceService);
|
|
1131
|
+
container.bind(TrustAwarePreferenceReader).toConstantValue(makeTrustAwareReader());
|
|
1132
|
+
container.bind(EnvVariablesServer).toConstantValue(makeEnvVariablesServer());
|
|
1133
|
+
container.bind(WorkspaceFunctionScope).toSelf();
|
|
1134
|
+
container.bind(GetWorkspaceDirectoryStructure).toSelf();
|
|
1135
|
+
|
|
1136
|
+
tool = container.get(GetWorkspaceDirectoryStructure);
|
|
1137
|
+
});
|
|
1138
|
+
|
|
1139
|
+
it('includes empty directories and excludes user-excluded ones', async () => {
|
|
1140
|
+
const result = await tool.getTool().handler(JSON.stringify({}), undefined);
|
|
1141
|
+
expect(result).to.deep.equal({
|
|
1142
|
+
workspace: {
|
|
1143
|
+
src: { nested: {} },
|
|
1144
|
+
empty: {}
|
|
1145
|
+
}
|
|
1146
|
+
});
|
|
1147
|
+
});
|
|
1148
|
+
});
|
|
1149
|
+
|
|
772
1150
|
// ── HEAD: External allowed paths tests ──────────────────────────────────────
|
|
773
1151
|
|
|
774
1152
|
describe('FileContentFunction external paths', () => {
|
|
@@ -1041,12 +1419,9 @@ describe('FindFilesByPattern with searchRoot', () => {
|
|
|
1041
1419
|
let findFilesByPattern: FindFilesByPattern;
|
|
1042
1420
|
let allowedPaths: string[];
|
|
1043
1421
|
|
|
1044
|
-
const
|
|
1045
|
-
'file:///workspace':
|
|
1046
|
-
'file:///
|
|
1047
|
-
'file:///external/configs': { isDirectory: true, children: ['file:///external/configs/myapp.json', 'file:///external/configs/other.txt'] },
|
|
1048
|
-
'file:///external/configs/myapp.json': { isDirectory: false },
|
|
1049
|
-
'file:///external/configs/other.txt': { isDirectory: false }
|
|
1422
|
+
const FILES_BY_ROOT: Record<string, string[]> = {
|
|
1423
|
+
'file:///workspace': ['file:///workspace/a.ts'],
|
|
1424
|
+
'file:///external/configs': ['file:///external/configs/myapp.json', 'file:///external/configs/other.txt']
|
|
1050
1425
|
};
|
|
1051
1426
|
|
|
1052
1427
|
let disableJSDOMInner: () => void;
|
|
@@ -1065,24 +1440,8 @@ describe('FindFilesByPattern with searchRoot', () => {
|
|
|
1065
1440
|
|
|
1066
1441
|
const mockFileService = {
|
|
1067
1442
|
exists: async () => true,
|
|
1068
|
-
resolve: async (uri: URI) => {
|
|
1069
|
-
|
|
1070
|
-
if (!node) {
|
|
1071
|
-
throw new Error('not found');
|
|
1072
|
-
}
|
|
1073
|
-
return {
|
|
1074
|
-
isDirectory: node.isDirectory,
|
|
1075
|
-
isFile: !node.isDirectory,
|
|
1076
|
-
resource: uri,
|
|
1077
|
-
children: node.children?.map(child => ({
|
|
1078
|
-
isDirectory: !!FS_TREE[child]?.isDirectory,
|
|
1079
|
-
isFile: !FS_TREE[child]?.isDirectory,
|
|
1080
|
-
resource: new URI(child),
|
|
1081
|
-
path: { base: child.split('/').pop() }
|
|
1082
|
-
})) ?? []
|
|
1083
|
-
};
|
|
1084
|
-
},
|
|
1085
|
-
read: async () => ({ value: '' })
|
|
1443
|
+
resolve: async (uri: URI) => ({ isDirectory: true, children: [], resource: uri }),
|
|
1444
|
+
read: async () => ({ value: { toString: () => '' } })
|
|
1086
1445
|
} as unknown as FileService;
|
|
1087
1446
|
|
|
1088
1447
|
const mockPreferenceService = {
|
|
@@ -1100,6 +1459,7 @@ describe('FindFilesByPattern with searchRoot', () => {
|
|
|
1100
1459
|
container.bind(PreferenceService).toConstantValue(mockPreferenceService);
|
|
1101
1460
|
container.bind(TrustAwarePreferenceReader).toConstantValue(trustAwareReader);
|
|
1102
1461
|
container.bind(EnvVariablesServer).toConstantValue(makeEnvVariablesServer());
|
|
1462
|
+
container.bind(FileSearchService).toConstantValue(makeRipgrepLikeSearchService(FILES_BY_ROOT));
|
|
1103
1463
|
container.bind(WorkspaceFunctionScope).toSelf();
|
|
1104
1464
|
container.bind(FindFilesByPattern).toSelf();
|
|
1105
1465
|
|