package cc.unitmesh.devins.document import cc.unitmesh.codegraph.CodeGraphFactory import cc.unitmesh.codegraph.model.CodeElementType import cc.unitmesh.codegraph.model.CodeNode import cc.unitmesh.codegraph.parser.CodeParser import cc.unitmesh.codegraph.parser.Language import io.github.oshai.kotlinlogging.KotlinLogging private val logger = KotlinLogging.logger {} /** * Code document parser using mpp-codegraph * * Parses source code files (Java, Kotlin, Python, JavaScript, TypeScript, etc.) * and extracts code structure (classes, methods, functions) as a hierarchical * document structure compatible with DocumentFile/TOCItem model. * * Supported Languages: * - Java (.java) * - Kotlin (.kt, .kts) * - JavaScript (.js) * - TypeScript (.ts, .tsx) * - Python (.py) * - Go (.go) * - Rust (.rs) * - C# (.cs) */ class CodeDocumentParser : DocumentParserService { private val codeParser: CodeParser = CodeGraphFactory.createParser() private var currentCodeNodes: List = emptyList() private var currentContent: String? = null override fun getDocumentContent(): String? = currentContent /** * Parse source code file and build hierarchical structure */ override suspend fun parse(file: DocumentFile, content: String): DocumentTreeNode { logger.debug { "=== Starting Code Parse ===" } logger.debug { "File: ${file.path}, Size: ${content.length} bytes" } try { currentContent = content // Detect language from file extension val language = detectLanguage(file.name) if (language == Language.UNKNOWN) { logger.warn { "Unknown language for file: ${file.name}" } return file.copy( metadata = file.metadata.copy( parseStatus = ParseStatus.PARSE_FAILED ) ) } logger.debug { "Detected language: $language" } // Parse code using mpp-codegraph currentCodeNodes = codeParser.parseNodes(content, file.path, language) logger.debug { "Extracted ${currentCodeNodes.size} code nodes" } // Build TOC from code structure (classes -> methods -> fields) val toc = buildTOCFromCodeNodes(currentCodeNodes) logger.debug { "Built TOC with ${toc.size} top-level items" } // Build entities (classes, functions) val entities = buildEntitiesFromCodeNodes(currentCodeNodes) logger.debug { "Built ${entities.size} entities" } logger.debug { "=== Parse Complete ===" } return file.copy( toc = toc, entities = entities, metadata = file.metadata.copy( parseStatus = ParseStatus.PARSED, chapterCount = toc.size, language = language.name.lowercase() ) ) } catch (e: Exception) { logger.error(e) { "Failed to parse code: ${e.message}" } return file.copy( metadata = file.metadata.copy( parseStatus = ParseStatus.PARSE_FAILED ) ) } } /** * Parse from ByteArray (delegates to parse with String conversion) */ override suspend fun parseBytes(file: DocumentFile, bytes: ByteArray): DocumentTreeNode { val content = String(bytes, Charsets.UTF_8) return parse(file, content) } /** * Query code by heading (class/method name) */ override suspend fun queryHeading(keyword: String): List { if (currentContent == null) return emptyList() val matchingNodes = currentCodeNodes.filter { node -> node.name.contains(keyword, ignoreCase = true) || node.qualifiedName.contains(keyword, ignoreCase = true) } return matchingNodes.map { node -> val nodeContent = extractNodeContent(node, currentContent!!) DocumentChunk( documentPath = node.filePath, chapterTitle = buildNodeTitle(node), content = nodeContent, anchor = "#${node.id}", startLine = node.startLine, endLine = node.endLine, position = PositionMetadata( documentPath = node.filePath, formatType = DocumentFormatType.SOURCE_CODE, position = DocumentPosition.LineRange( startLine = node.startLine, endLine = node.endLine ) ) ) } } /** * Query code by chapter ID (node ID) */ override suspend fun queryChapter(chapterId: String): DocumentChunk? { if (currentContent == null) return null val node = currentCodeNodes.find { it.id == chapterId || it.id == chapterId.removePrefix("#") } ?: return null val nodeContent = extractNodeContent(node, currentContent!!) return DocumentChunk( documentPath = node.filePath, chapterTitle = buildNodeTitle(node), content = nodeContent, anchor = "#${node.id}", startLine = node.startLine, endLine = node.endLine, position = PositionMetadata( documentPath = node.filePath, formatType = DocumentFormatType.SOURCE_CODE, position = DocumentPosition.LineRange( startLine = node.startLine, endLine = node.endLine ) ) ) } /** * Build TOC from code nodes (hierarchical: packages -> classes -> methods -> fields) */ private fun buildTOCFromCodeNodes(nodes: List): List { // Group nodes by type and hierarchy val packages = mutableMapOf>() val topLevelClasses = mutableListOf() for (node in nodes) { when (node.type) { CodeElementType.PACKAGE -> { // Skip packages in TOC, we'll use them for grouping } CodeElementType.CLASS, CodeElementType.INTERFACE, CodeElementType.ENUM -> { if (node.packageName.isNotEmpty()) { packages.getOrPut(node.packageName) { mutableListOf() }.add(node) } else { topLevelClasses.add(node) } } else -> { // Methods, fields, etc. will be nested under classes } } } val toc = mutableListOf() // Add package groups for ((packageName, classNodes) in packages.toSortedMap()) { val packageItem = TOCItem( level = 1, title = "📦 $packageName", anchor = "#package-${packageName.replace('.', '-')}", lineNumber = classNodes.minOfOrNull { it.startLine }, children = classNodes.map { classNode -> buildClassTOCItem(classNode, nodes, level = 2) } ) toc.add(packageItem) } // Add top-level classes (no package) for (classNode in topLevelClasses) { toc.add(buildClassTOCItem(classNode, nodes, level = 1)) } return toc } /** * Build TOC item for a class and its members */ private fun buildClassTOCItem(classNode: CodeNode, allNodes: List, level: Int): TOCItem { val classQualifiedName = classNode.qualifiedName // Find all members of this class (methods, fields) val members = allNodes.filter { node -> node.qualifiedName.startsWith("$classQualifiedName.") && node.qualifiedName.count { it == '.' } == classQualifiedName.count { it == '.' } + 1 } val children = members.map { member -> val icon = when (member.type) { CodeElementType.METHOD, CodeElementType.FUNCTION -> "⚡" CodeElementType.FIELD, CodeElementType.PROPERTY -> "📌" else -> "•" } TOCItem( level = level + 1, title = "$icon ${member.name}", anchor = "#${member.id}", lineNumber = member.startLine ) } val classIcon = when (classNode.type) { CodeElementType.CLASS -> "📘" CodeElementType.INTERFACE -> "🔷" CodeElementType.ENUM -> "🔢" else -> "•" } return TOCItem( level = level, title = "$classIcon ${classNode.name}", anchor = "#${classNode.id}", lineNumber = classNode.startLine, children = children ) } /** * Build entities from code nodes */ private fun buildEntitiesFromCodeNodes(nodes: List): List { return nodes.mapNotNull { node -> when (node.type) { CodeElementType.CLASS, CodeElementType.INTERFACE, CodeElementType.ENUM -> { Entity.ClassEntity( name = node.name, packageName = node.packageName, location = Location( anchor = "#${node.id}", line = node.startLine ) ) } CodeElementType.METHOD, CodeElementType.FUNCTION -> { Entity.FunctionEntity( name = node.name, signature = extractSignature(node), location = Location( anchor = "#${node.id}", line = node.startLine ) ) } else -> null } } } /** * Extract node content from source code */ private fun extractNodeContent(node: CodeNode, sourceCode: String): String { return if (node.content.isNotEmpty()) { node.content } else { // Fallback: extract by line numbers val lines = sourceCode.lines() if (node.startLine <= lines.size && node.endLine <= lines.size) { lines.subList(node.startLine - 1, node.endLine).joinToString("\n") } else { "" } } } /** * Build display title for a node */ private fun buildNodeTitle(node: CodeNode): String { return when (node.type) { CodeElementType.CLASS -> "class ${node.name}" CodeElementType.INTERFACE -> "interface ${node.name}" CodeElementType.ENUM -> "enum ${node.name}" CodeElementType.METHOD, CodeElementType.FUNCTION -> { val signature = extractSignature(node) if (signature != null) "$signature" else node.name } else -> node.name } } /** * Extract method/function signature from node metadata */ private fun extractSignature(node: CodeNode): String? { return node.metadata["signature"] ?: node.metadata["parameters"]?.let { params -> "${node.name}($params)" } } /** * Detect programming language from file extension */ private fun detectLanguage(fileName: String): Language { val extension = fileName.substringAfterLast('.', "").lowercase() return when (extension) { "java" -> Language.JAVA "kt", "kts" -> Language.KOTLIN "js" -> Language.JAVASCRIPT "ts", "tsx" -> Language.TYPESCRIPT "py" -> Language.PYTHON "go" -> Language.GO "rs" -> Language.RUST "cs" -> Language.CSHARP else -> Language.UNKNOWN } } }