Skip to content

Commit 0815844

Browse files
committed
Add support for search in workspace
fixes #15467
1 parent a830ffa commit 0815844

File tree

5 files changed

+149
-0
lines changed

5 files changed

+149
-0
lines changed

package-lock.json

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/ai-ide/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
"@theia/terminal": "1.60.0",
2727
"@theia/workspace": "1.60.0",
2828
"@theia/ai-mcp": "1.60.0",
29+
"@theia/search-in-workspace": "1.60.0",
2930
"ignore": "^6.0.0",
3031
"minimatch": "^9.0.0",
3132
"date-fns": "^4.1.0"

packages/ai-ide/src/browser/frontend-module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { Agent, AIVariableContribution, bindToolProvider } from '@theia/ai-core/
2020
import { ArchitectAgent } from './architect-agent';
2121
import { CoderAgent } from './coder-agent';
2222
import { FileContentFunction, FileDiagonsticProvider, GetWorkspaceDirectoryStructure, GetWorkspaceFileList, WorkspaceFunctionScope } from './workspace-functions';
23+
import { WorkspaceSearchProvider } from './workspace-search-provider';
2324
import { FrontendApplicationContribution, PreferenceContribution, WidgetFactory, bindViewContribution } from '@theia/core/lib/browser';
2425
import { WorkspacePreferencesSchema } from './workspace-preferences';
2526
import {
@@ -79,6 +80,7 @@ export default new ContainerModule(bind => {
7980
bindToolProvider(GetWorkspaceDirectoryStructure, bind);
8081
bindToolProvider(FileDiagonsticProvider, bind);
8182
bind(WorkspaceFunctionScope).toSelf().inSingletonScope();
83+
bindToolProvider(WorkspaceSearchProvider, bind);
8284

8385
bindToolProvider(WriteChangeToFileProvider, bind);
8486
bind(ReplaceContentInFileFunctionHelper).toSelf().inSingletonScope();
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
// *****************************************************************************
2+
// Copyright (C) 2024 EclipseSource GmbH.
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 { MutableChatRequestModel } from '@theia/ai-chat';
18+
import { ToolProvider, ToolRequest } from '@theia/ai-core';
19+
import { CancellationToken } from '@theia/core';
20+
import { inject, injectable } from '@theia/core/shared/inversify';
21+
import { SearchInWorkspaceService, SearchInWorkspaceCallbacks } from '@theia/search-in-workspace/lib/browser/search-in-workspace-service';
22+
import { SearchInWorkspaceResult, SearchInWorkspaceOptions } from '@theia/search-in-workspace/lib/common/search-in-workspace-interface';
23+
24+
const SEARCH_IN_WORKSPACE_ID = 'searchInWorkspace';
25+
@injectable()
26+
export class WorkspaceSearchProvider implements ToolProvider {
27+
28+
@inject(SearchInWorkspaceService)
29+
protected readonly searchService: SearchInWorkspaceService;
30+
31+
private readonly MAX_RESULTS = 50;
32+
33+
getTool(): ToolRequest {
34+
return {
35+
id: SEARCH_IN_WORKSPACE_ID,
36+
name: SEARCH_IN_WORKSPACE_ID,
37+
description: 'Searches the content of files within the workspace for lines matching the given search term (`query`). \
38+
The search uses case-insensitive string matching or regular expressions (controlled by the `useRegExp` parameter). \
39+
It returns a list of matching files, including the file path (URI), the line number, and the full text content of each matching line. \
40+
Multi-word patterns must match exactly (including spaces, case-insensitively). \
41+
For complex searches, prefer multiple simpler queries over one complex query or regular expression.',
42+
parameters: {
43+
type: 'object',
44+
properties: {
45+
query: {
46+
type: 'string',
47+
description: 'The search term or regular expression pattern.',
48+
},
49+
useRegExp: {
50+
type: 'boolean',
51+
description: 'Set to true if the query is a regular expression.',
52+
}
53+
},
54+
required: ['query', 'useRegExp']
55+
},
56+
handler: (argString, ctx: MutableChatRequestModel) => this.handleSearch(argString, ctx?.response?.cancellationToken)
57+
};
58+
}
59+
60+
private async handleSearch(argString: string, cancellationToken?: CancellationToken): Promise<string> {
61+
try {
62+
const args: { query: string, useRegExp: boolean } = JSON.parse(argString);
63+
const results: SearchInWorkspaceResult[] = [];
64+
let expectedSearchId: number | undefined;
65+
let searchCompleted = false;
66+
67+
const searchPromise = new Promise<SearchInWorkspaceResult[]>((resolve, reject) => {
68+
const callbacks: SearchInWorkspaceCallbacks = {
69+
onResult: (id, result) => {
70+
if (expectedSearchId !== undefined && id !== expectedSearchId) {
71+
return;
72+
}
73+
74+
if (searchCompleted) {
75+
return;
76+
}
77+
78+
results.push(result);
79+
},
80+
onDone: (id, error) => {
81+
if (expectedSearchId !== undefined && id !== expectedSearchId) {
82+
return;
83+
}
84+
85+
if (searchCompleted) {
86+
return;
87+
}
88+
89+
searchCompleted = true;
90+
if (error) {
91+
reject(new Error(`Search failed: ${error}`));
92+
} else {
93+
resolve(results);
94+
}
95+
}
96+
};
97+
98+
const options: SearchInWorkspaceOptions = {
99+
useRegExp: args.useRegExp,
100+
matchCase: false,
101+
matchWholeWord: false,
102+
maxResults: this.MAX_RESULTS,
103+
};
104+
105+
this.searchService.search(args.query, callbacks, options)
106+
.then(id => {
107+
expectedSearchId = id;
108+
cancellationToken?.onCancellationRequested(() => {
109+
this.searchService.cancel(id);
110+
});
111+
})
112+
.catch(err => {
113+
searchCompleted = true;
114+
reject(err);
115+
});
116+
117+
});
118+
119+
const timeoutPromise = new Promise<SearchInWorkspaceResult[]>((_, reject) => {
120+
setTimeout(() => {
121+
if (expectedSearchId !== undefined && !searchCompleted) {
122+
this.searchService.cancel(expectedSearchId);
123+
searchCompleted = true;
124+
reject(new Error('Search timed out after 30 seconds'));
125+
}
126+
}, 30000);
127+
});
128+
129+
const finalResults = await Promise.race([searchPromise, timeoutPromise]);
130+
131+
const formattedResults = finalResults.map(r => ({
132+
file: r.fileUri,
133+
matches: r.matches.map(m => ({ line: m.line, text: m.lineText }))
134+
}));
135+
136+
return JSON.stringify(formattedResults);
137+
138+
} catch (error) {
139+
return JSON.stringify({ error: error.message || 'Failed to execute search' });
140+
}
141+
}
142+
}

packages/ai-ide/tsconfig.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@
3939
{
4040
"path": "../navigator"
4141
},
42+
{
43+
"path": "../search-in-workspace"
44+
},
4245
{
4346
"path": "../terminal"
4447
},

0 commit comments

Comments
 (0)