Skip to content

Commit d40d5bc

Browse files
committed
feat(docql): prioritize exact matches for class and function queries
Improve class and function queries to return only exact name matches when available, preventing unrelated partial matches. Add utility functions to extract class and function names from titles.
1 parent dee2ef6 commit d40d5bc

File tree

1 file changed

+125
-9
lines changed

1 file changed

+125
-9
lines changed

mpp-core/src/commonMain/kotlin/cc/unitmesh/devins/document/docql/CodeDocQLExecutor.kt

Lines changed: 125 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,13 @@ class CodeDocQLExecutor(
260260
* Execute $.code.class("ClassName") - find specific class and its content
261261
*
262262
* Supports wildcard: $.code.class("*") returns all classes (equivalent to $.code.classes[*])
263+
*
264+
* Matching priority:
265+
* 1. Exact match: class name exactly equals the query (e.g., "CodingAgent" matches "class CodingAgent")
266+
* 2. Partial match: class name contains the query (e.g., "Agent" matches "CodingAgent", "CodingAgentContext")
267+
*
268+
* When exact match is found, only exact matches are returned.
269+
* This prevents returning unrelated classes like "CodingAgentContext" when searching for "CodingAgent".
263270
*/
264271
private suspend fun executeCodeClassQuery(className: String): DocQLResult {
265272
if (documentFile == null) {
@@ -283,14 +290,65 @@ class CodeDocQLExecutor(
283290
val title = chunk.chapterTitle ?: ""
284291
title.startsWith("class ") ||
285292
title.startsWith("interface ") ||
286-
title.startsWith("enum ")
293+
title.startsWith("enum ") ||
294+
title.startsWith("object ") // Kotlin objects
287295
}
288-
289-
return if (classChunks.isNotEmpty()) {
290-
DocQLResult.Chunks(mapOf(documentFile.path to classChunks))
296+
297+
if (classChunks.isEmpty()) {
298+
return DocQLResult.Empty
299+
}
300+
301+
// Prioritize exact matches over partial matches
302+
// Extract class name from title (e.g., "class CodingAgent" -> "CodingAgent", "class Foo<T>" -> "Foo")
303+
val exactMatches = classChunks.filter { chunk ->
304+
val extractedName = extractClassNameFromTitle(chunk.chapterTitle ?: "")
305+
extractedName.equals(className, ignoreCase = true)
306+
}
307+
308+
return if (exactMatches.isNotEmpty()) {
309+
// Return only exact matches - this prevents returning CodingAgentContext when searching for CodingAgent
310+
DocQLResult.Chunks(mapOf(documentFile.path to exactMatches))
291311
} else {
292-
DocQLResult.Empty
312+
// No exact match - return partial matches sorted by relevance
313+
// Sort: shorter names (more specific) come first
314+
val sortedChunks = classChunks.sortedBy { chunk ->
315+
val extractedName = extractClassNameFromTitle(chunk.chapterTitle ?: "")
316+
extractedName.length
317+
}
318+
DocQLResult.Chunks(mapOf(documentFile.path to sortedChunks))
319+
}
320+
}
321+
322+
/**
323+
* Extract the class/interface/enum name from a chunk title.
324+
* Examples:
325+
* - "class CodingAgent" -> "CodingAgent"
326+
* - "class Foo<T>" -> "Foo"
327+
* - "interface Service" -> "Service"
328+
* - "enum Status" -> "Status"
329+
* - "object Companion" -> "Companion"
330+
*/
331+
private fun extractClassNameFromTitle(title: String): String {
332+
val prefixes = listOf("class ", "interface ", "enum ", "object ")
333+
val withoutPrefix = prefixes.fold(title) { acc, prefix ->
334+
if (acc.startsWith(prefix, ignoreCase = true)) {
335+
acc.removePrefix(prefix).trimStart()
336+
} else {
337+
acc
338+
}
293339
}
340+
// Remove generic parameters (e.g., "Foo<T>" -> "Foo")
341+
val genericIndex = withoutPrefix.indexOf('<')
342+
val parenIndex = withoutPrefix.indexOf('(')
343+
val colonIndex = withoutPrefix.indexOf(':')
344+
val spaceIndex = withoutPrefix.indexOf(' ')
345+
346+
// Find the first delimiter
347+
val endIndex = listOf(genericIndex, parenIndex, colonIndex, spaceIndex)
348+
.filter { it > 0 }
349+
.minOrNull() ?: withoutPrefix.length
350+
351+
return withoutPrefix.substring(0, endIndex).trim()
294352
}
295353

296354
/**
@@ -322,6 +380,8 @@ class CodeDocQLExecutor(
322380
* Execute $.code.query("keyword") - custom query for any code element
323381
*
324382
* Supports wildcard: $.code.query("*") returns all code chunks
383+
*
384+
* For function queries, prioritizes exact name matches over partial matches.
325385
*/
326386
private suspend fun executeCodeCustomQuery(keyword: String): DocQLResult {
327387
if (parserService == null || documentFile == null) {
@@ -354,11 +414,67 @@ class CodeDocQLExecutor(
354414

355415
// Use heading query for flexible search
356416
val chunks = parserService.queryHeading(keyword)
357-
358-
return if (chunks.isNotEmpty()) {
359-
DocQLResult.Chunks(mapOf(documentFile.path to chunks))
417+
418+
if (chunks.isEmpty()) {
419+
return DocQLResult.Empty
420+
}
421+
422+
// Prioritize exact function name matches over partial matches
423+
val exactMatches = chunks.filter { chunk ->
424+
val title = chunk.chapterTitle ?: ""
425+
// Extract function name from title (e.g., "fun execute()" -> "execute")
426+
val funcName = extractFunctionNameFromTitle(title)
427+
funcName.equals(keyword, ignoreCase = true)
428+
}
429+
430+
return if (exactMatches.isNotEmpty()) {
431+
// Return exact matches first
432+
DocQLResult.Chunks(mapOf(documentFile.path to exactMatches))
360433
} else {
361-
DocQLResult.Empty
434+
// No exact match - return all matches sorted by relevance (shorter names first)
435+
val sortedChunks = chunks.sortedBy { chunk ->
436+
val title = chunk.chapterTitle ?: ""
437+
extractFunctionNameFromTitle(title).length
438+
}
439+
DocQLResult.Chunks(mapOf(documentFile.path to sortedChunks))
362440
}
363441
}
442+
443+
/**
444+
* Extract the function name from a chunk title.
445+
* Examples:
446+
* - "fun execute()" -> "execute"
447+
* - "fun execute(param: String)" -> "execute"
448+
* - "suspend fun process()" -> "process"
449+
* - "private fun helper()" -> "helper"
450+
*/
451+
private fun extractFunctionNameFromTitle(title: String): String {
452+
// Find "fun " keyword and extract the function name after it
453+
val funIndex = title.indexOf("fun ")
454+
if (funIndex >= 0) {
455+
val afterFun = title.substring(funIndex + 4).trimStart()
456+
// Function name ends at ( or <
457+
val parenIndex = afterFun.indexOf('(')
458+
val genericIndex = afterFun.indexOf('<')
459+
val endIndex = listOf(parenIndex, genericIndex)
460+
.filter { it > 0 }
461+
.minOrNull() ?: afterFun.length
462+
return afterFun.substring(0, endIndex).trim()
463+
}
464+
465+
// For class methods without "fun" prefix, try to extract method name
466+
val parenIndex = title.indexOf('(')
467+
if (parenIndex > 0) {
468+
// Find the last word before (
469+
val beforeParen = title.substring(0, parenIndex).trim()
470+
val lastSpaceIndex = beforeParen.lastIndexOf(' ')
471+
return if (lastSpaceIndex >= 0) {
472+
beforeParen.substring(lastSpaceIndex + 1)
473+
} else {
474+
beforeParen
475+
}
476+
}
477+
478+
return title
479+
}
364480
}

0 commit comments

Comments
 (0)