@@ -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