Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 13 additions & 46 deletions src/com/lsfusion/mcp/MCPSearchUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@
import com.intellij.psi.PsiReference;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.psi.search.ProjectScope;
import com.intellij.psi.search.PsiSearchHelper;
import com.intellij.psi.search.UsageSearchContext;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.util.MergeQuery;
import com.intellij.util.Processor;
Expand Down Expand Up @@ -110,8 +108,7 @@ private SelectedStatement(@NotNull LSFMCPDeclaration decl, GlobalSearchScope sea
* // scope=project -> projectOnly scope (excludes libraries)
* // otherwise, scope can be a CSV list of IDEA module names (e.g. "my-idea-module1,my-idea-module2")
* requiredModules: true, // optional (default: true); if true, include REQUIRE-d modules for modules in `modules`
* name: "cust,Order", // optional, CSV; filters by element name
* contains: "(?i)cust.*", // optional, CSV; filters by element code
* query: "cust,Order", // optional, CSV; filters by element name and code (word or regex)
* elementTypes: "class,property,action", // optional, CSV
* classes: "MyNS.MyClass, MyOtherNS.OtherClass", // optional, CSV canonical names
* relatedElements: "property:MyNS.myProp[MyNS.MyClass], MyModule(10:5)", // optional, CSV
Expand Down Expand Up @@ -240,7 +237,7 @@ private static JSONObject assembleResult(JSONArray items, String meta, List<Stri
return resultObject;
}

private static Processor<LSFMCPDeclaration> createSearchProcessor(SearchState state, Set<LSFMCPDeclaration> seen, List<NameFilter> nameFilters, List<NameFilter> containsFilters, Set<LSFMCPDeclaration.ElementType> elementTypes, Set<LSFClassDeclaration> classDecls, Map<LSFMCPDeclaration, Direction> related, GlobalSearchScope searchScope, ConcurrentMap<RelatedKey, RelatedState> relatedCache) {
private static Processor<LSFMCPDeclaration> createSearchProcessor(SearchState state, Set<LSFMCPDeclaration> seen, List<NameFilter> queryFilters, Set<LSFMCPDeclaration.ElementType> elementTypes, Set<LSFClassDeclaration> classDecls, Map<LSFMCPDeclaration, Direction> related, GlobalSearchScope searchScope, ConcurrentMap<RelatedKey, RelatedState> relatedCache) {
return st -> {
if (state.stopRequested.get()) {
return false;
Expand All @@ -254,7 +251,7 @@ private static Processor<LSFMCPDeclaration> createSearchProcessor(SearchState st

// already processed

if (matchesAllFilters(st, nameFilters, containsFilters, elementTypes, classDecls, related, searchScope, relatedCache)) {
if (matchesAllFilters(st, queryFilters, elementTypes, classDecls, related, searchScope, relatedCache)) {
SelectedStatement selSt = new SelectedStatement(st, searchScope);

synchronized (state.statementsByPriority[p]) {
Expand Down Expand Up @@ -371,41 +368,21 @@ private static void submitFileTasks(GlobalSearchScope searchScope, Processor<LSF
}
}

private static boolean submitWordTasks(Project project, GlobalSearchScope searchScope, List<NameFilter> filters, Processor<LSFMCPDeclaration> processor, TaskSubmitter submit) {
if (filters.isEmpty()) return false;
boolean fullyStreamable = true;
for (NameFilter nf : filters) {
if (nf.isWordStreamable()) {
submit.submit(5, () -> ReadAction.run(() -> streamWord(project, nf, processor, searchScope)));
} else {
fullyStreamable = false;
}
}
return fullyStreamable;
}

private static @NotNull GlobalSearchScope run(@NotNull Project project,
@NotNull JSONObject query,
@NotNull Set<LSFMCPDeclaration> seen,
@NotNull ConcurrentMap<RelatedKey, RelatedState> relatedCache,
@NotNull SearchState state,
@NotNull TaskSubmitter submit) {
GlobalSearchScope searchScope = ReadAction.compute(() -> buildSearchScope(project, query.optString("modules"), query.optString("scope"), query.optBoolean("requiredModules", true)));
List<NameFilter> nameFilters = parseMatchersCsv(query.optString("name"));
List<NameFilter> containsFilters = parseMatchersCsv(query.optString("contains"));
List<NameFilter> queryFilters = parseMatchersCsv(query.optString("query"));
Set<LSFMCPDeclaration.ElementType> elementTypes = parseElementTypes(query.optString("elementTypes"));

Set<LSFClassDeclaration> classDecls = ReadAction.compute(() -> parseClasses(project, searchScope, query.optString("classes")));
Map<LSFMCPDeclaration, Direction> related = ReadAction.compute(() -> parseRelated(project, searchScope, query.optString("relatedElements"), query.optString("relatedDirection")));

// Shared processor that applies all filters and returns false to stop the current iteration
final Processor<LSFMCPDeclaration> processor = createSearchProcessor(state, seen, nameFilters, containsFilters, elementTypes, classDecls, related, searchScope, relatedCache);

// Track which blocks are fully streamable while assembling iterations.
// Name/code filters are considered fully streamable only if ALL matchers are "word-only" with length >= 3.
// Name/code-based iterations (only for word length >= 3)
boolean nameFullyStreamable = submitWordTasks(project, searchScope, nameFilters, processor, submit);
boolean containsFullyStreamable = submitWordTasks(project, searchScope, containsFilters, processor, submit);
final Processor<LSFMCPDeclaration> processor = createSearchProcessor(state, seen, queryFilters, elementTypes, classDecls, related, searchScope, relatedCache);

// Element-type iterations (only for index-backed types and only if elementTypes filter provided)
boolean typesFullyStreamable = submitTypeTasks(project, searchScope, elementTypes, processor, submit);
Expand All @@ -417,7 +394,7 @@ private static boolean submitWordTasks(Project project, GlobalSearchScope search
boolean relatedFullyStreamable = submitRelatedTasks(related, searchScope, processor, submit);

// Per-file iterations (fallback) — run only if no block is fully streamable
if (!nameFullyStreamable && !containsFullyStreamable && !classesFullyStreamable && !relatedFullyStreamable && !typesFullyStreamable) {
if (!classesFullyStreamable && !relatedFullyStreamable && !typesFullyStreamable) {
submitFileTasks(searchScope, processor, submit);
}

Expand All @@ -432,15 +409,6 @@ private static boolean isOnlyPropertiesClassesActions(Set<LSFMCPDeclaration.Elem
return onlyPropertiesClassesActions;
}

private static void streamWord(@NonNull Project project, NameFilter nf, Processor<LSFMCPDeclaration> processor, GlobalSearchScope searchScope) {
PsiSearchHelper helper = PsiSearchHelper.getInstance(project);
helper.processElementsWithWord((element, offsetInElement) -> processStatement(element, processor),
searchScope,
nf.word,
(short)(UsageSearchContext.IN_CODE | UsageSearchContext.IN_FOREIGN_LANGUAGES | UsageSearchContext.IN_COMMENTS),
true);
}

private static boolean isTimedOut(long deadlineMillis) {
return System.currentTimeMillis() > deadlineMillis;
}
Expand Down Expand Up @@ -1012,21 +980,24 @@ private static GlobalSearchScope buildSearchScope(Project project, String module
// region Matching

private static boolean matchesAllFilters(LSFMCPDeclaration stmt,
List<NameFilter> nameFilters,
List<NameFilter> containsFilters,
List<NameFilter> queryFilters,
Set<LSFMCPDeclaration.ElementType> elementTypes,
Set<LSFClassDeclaration> classDecls,
Map<LSFMCPDeclaration, Direction> related,
GlobalSearchScope scope,
ConcurrentMap<RelatedKey, RelatedState> relatedCache) {
LSFMCPDeclaration.ElementType t;
return (elementTypes.isEmpty() || ((t = stmt.getMCPType()) != null && elementTypes.contains(t))) &&
(nameFilters.isEmpty() || matchesNameFilters(stmt, nameFilters, scope, false)) &&
(containsFilters.isEmpty() || matchesNameFilters(stmt, containsFilters, scope, true)) &&
(queryFilters.isEmpty() || matchesQueryFilters(stmt, queryFilters, scope)) &&
(classDecls.isEmpty() || matchesClassesFilter(stmt, classDecls, scope)) &&
(related.isEmpty() || matchesRelatedFilters(stmt, related, scope, relatedCache));
}

private static boolean matchesQueryFilters(LSFMCPDeclaration stmt, List<NameFilter> queryFilters, GlobalSearchScope scope) {
return matchesNameFilters(stmt, queryFilters, scope, false) ||
matchesNameFilters(stmt, queryFilters, scope, true);
}

private static boolean matchesClassesFilter(LSFMCPDeclaration stmt, Set<LSFClassDeclaration> classDecls, GlobalSearchScope scope) {
if (classDecls.isEmpty()) return true;
// Resolve candidate declarations (can be several)
Expand Down Expand Up @@ -1262,10 +1233,6 @@ private static boolean matchesNameFilters(LSFMCPDeclaration stmt, List<NameFilte
private static class NameFilter {
final String word;
final Pattern regex;

public boolean isWordStreamable() {
return word != null && word.length() >= 3 && regex == null;
}

NameFilter(String word, Pattern regex) {
this.word = word;
Expand Down
10 changes: 3 additions & 7 deletions src/com/lsfusion/mcp/McpServerService.java
Original file line number Diff line number Diff line change
Expand Up @@ -250,14 +250,10 @@ private JSONObject buildFindElementsToolDescriptor() {
.put("type", "string")
.put("description",
"Scope filter (IDEA concept): omitted = project + libraries; `project` = project content only; otherwise, a CSV list of IDEA module names."))
.put("name", new JSONObject()
.put("type", "string")
.put("description",
"Element name filter as CSV (comma-separated). Word if valid ID, else Java regex."))
.put("contains", new JSONObject()
.put("query", new JSONObject()
.put("type", "string")
.put("description",
"Element code filter as CSV. Word if valid ID, else Java regex."))
"Query filter as CSV (comma-separated). Word if valid ID, else Java regex. Matches against element names and code."))
.put("elementTypes", new JSONObject()
.put("type", "string")
.put("description", "Element type filter as CSV. Allowed values: `module`, `metacode`, `class`, `property`, `action`, `form`, `navigatorElement`, `window`, `group`, `table`, `event`, `calculatedEvent`, `constraint`, `index`."))
Expand Down Expand Up @@ -285,7 +281,7 @@ private JSONObject buildFindElementsToolDescriptor() {
.put("moreFilters", new JSONObject()
.put("type", "string")
.put("description",
"Additional filter objects of the same structure as the root. JSON array string (e.g. `[{\"names\":\"Foo\", \"modules\" : \"MyModule\"},{\"names\":\"Bar\"}]`). Results are merged (OR)."))
"Additional filter objects of the same structure as the root. JSON array string (e.g. `[{\"query\":\"Foo\", \"modules\" : \"MyModule\"},{\"query\":\"Bar\"}]`). Results are merged (OR)."))
.put("minSymbols", new JSONObject()
.put("type", "integer")
.put("minimum", 0)
Expand Down
39 changes: 17 additions & 22 deletions src/com/lsfusion/mcp/McpToolset.kt
Original file line number Diff line number Diff line change
Expand Up @@ -193,13 +193,9 @@ class McpToolset : com.intellij.mcpserver.McpToolset {
)
scope: String? = null,
@McpDescription(
description = "Element name filter as CSV (comma-separated). Word if valid ID, else Java regex."
description = "Query filter as CSV (comma-separated). Word if valid ID, else Java regex. Matches against element names and code."
)
names: String? = null,
@McpDescription(
description = "Element code filter as CSV. Word if valid ID, else Java regex."
)
contains: String? = null,
queryText: String? = null,
@McpDescription(
description = "Element type filter as CSV. Allowed values: `module`, `metacode`, `class`, `property`, `action`, `form`, `navigatorElement`, `window`, `group`, `table`, `event`, `calculatedEvent`, `constraint`, `index`."
)
Expand All @@ -215,7 +211,7 @@ class McpToolset : com.intellij.mcpserver.McpToolset {
@McpDescription(description = "Direction for ALL `relatedElements` seeds. Allowed values: `both`, `uses`, `used`. Default: `both`.")
relatedDirection: String? = null,
@McpDescription(
description = "Additional filter objects of the same structure as the root. JSON array string (e.g. `[{\"names\":\"Foo\", \"modules\" : \"MyModule\"},{\"names\":\"Bar\"}]`). Results are merged (OR)."
description = "Additional filter objects of the same structure as the root. JSON array string (e.g. `[{\"query\":\"Foo\", \"modules\" : \"MyModule\"},{\"query\":\"Bar\"}]`). Results are merged (OR)."
)
moreFilters: String? = null,
@McpDescription(description = "Best-effort minimum output size in JSON chars; server may append neighboring elements if too small (>= 0). Default: ${MCPSearchUtils.DEFAULT_MIN_SYMBOLS}.")
Expand All @@ -235,24 +231,23 @@ class McpToolset : com.intellij.mcpserver.McpToolset {
}

try {
val query = JSONObject()
if (modules != null) query.put("modules", modules)
if (scope != null) query.put("scope", scope)
query.put("requiredModules", requiredModules)
if (names != null) query.put("name", names)
if (contains != null) query.put("contains", contains)
if (elementTypes != null) query.put("elementTypes", elementTypes)
if (classes != null) query.put("classes", classes)
if (relatedElements != null) query.put("relatedElements", relatedElements)
if (relatedDirection != null) query.put("relatedDirection", relatedDirection)
query.put("minSymbols", minSymbols)
query.put("maxSymbols", maxSymbols)
query.put("timeoutSeconds", timeoutSeconds)
val payload = JSONObject()
if (modules != null) payload.put("modules", modules)
if (scope != null) payload.put("scope", scope)
payload.put("requiredModules", requiredModules)
if (queryText != null) payload.put("query", queryText)
if (elementTypes != null) payload.put("elementTypes", elementTypes)
if (classes != null) payload.put("classes", classes)
if (relatedElements != null) payload.put("relatedElements", relatedElements)
if (relatedDirection != null) payload.put("relatedDirection", relatedDirection)
payload.put("minSymbols", minSymbols)
payload.put("maxSymbols", maxSymbols)
payload.put("timeoutSeconds", timeoutSeconds)
if (moreFilters != null && !moreFilters.isEmpty()) {
query.put("moreFilters", JSONArray(moreFilters))
payload.put("moreFilters", JSONArray(moreFilters))
}

val result = MCPSearchUtils.findElements(project, query)
val result = MCPSearchUtils.findElements(project, payload)
val jsonElement = json.parseToJsonElement(result.toString())
return json.decodeFromJsonElement<FindElementsResult>(jsonElement)
} catch (e: McpExpectedError) {
Expand Down