@theia/ai-ide 1.72.0 → 1.72.2

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.
@@ -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 FS_TREE: Record<string, { isDirectory: boolean; children?: string[] }> = {
1045
- 'file:///workspace': { isDirectory: true, children: ['file:///workspace/a.ts'] },
1046
- 'file:///workspace/a.ts': { isDirectory: false },
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
- const node = FS_TREE[uri.toString()];
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