Skip to content

Commit d14eea4

Browse files
committed
fix(document): improve document loading and parsing robustness #463
Handle file read and parse errors gracefully, clear stale content on failure, and add bounds checking for heading offsets in Markdown parser. Also reset DocQL state when document changes and simplify document content rendering.
1 parent fe19e5e commit d14eea4

File tree

6 files changed

+110
-96
lines changed

6 files changed

+110
-96
lines changed

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

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -185,10 +185,12 @@ class MarkdownDocumentParser : DocumentParserService {
185185
val lines = content.lines()
186186

187187
headings.forEachIndexed { index, heading ->
188-
// Calculate line numbers
189-
val startLine = content.substring(0, heading.endOffset).count { it == '\n' }
188+
// Calculate line numbers with bounds checking
189+
val safeEndOffset = minOf(heading.endOffset, content.length)
190+
val startLine = content.substring(0, safeEndOffset).count { it == '\n' }
190191
val endLine = if (index < headings.size - 1) {
191-
content.substring(0, headings[index + 1].startOffset).count { it == '\n' } - 1
192+
val safeNextStartOffset = minOf(headings[index + 1].startOffset, content.length)
193+
content.substring(0, safeNextStartOffset).count { it == '\n' } - 1
192194
} else {
193195
lines.size - 1
194196
}
@@ -201,16 +203,16 @@ class MarkdownDocumentParser : DocumentParserService {
201203

202204
val anchor = "#${heading.text.lowercase().replace(Regex("[^a-z0-9]+"), "-")}"
203205

204-
// Create position metadata
206+
// Create position metadata with bounds checking
205207
val positionMetadata = PositionMetadata(
206208
documentPath = documentPath,
207209
formatType = DocumentFormatType.MARKDOWN,
208210
position = DocumentPosition.LineRange(
209211
startLine = startLine,
210212
endLine = endLine,
211-
startOffset = heading.startOffset,
213+
startOffset = minOf(heading.startOffset, content.length),
212214
endOffset = if (index < headings.size - 1) {
213-
headings[index + 1].startOffset - 1
215+
minOf(headings[index + 1].startOffset - 1, content.length)
214216
} else {
215217
content.length
216218
}

mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/document/DocumentNavigationPane.kt

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -258,15 +258,13 @@ private fun DocumentFileItem(
258258
verticalAlignment = Alignment.CenterVertically,
259259
horizontalArrangement = Arrangement.spacedBy(8.dp)
260260
) {
261-
// 文档类型图标
262261
Icon(
263262
imageVector = AutoDevComposeIcons.Article,
264263
contentDescription = null,
265264
modifier = Modifier.size(18.dp),
266265
tint = MaterialTheme.colorScheme.secondary
267266
)
268267

269-
// 文档名称
270268
Column(modifier = Modifier.weight(1f)) {
271269
Text(
272270
text = file.name,
@@ -275,7 +273,6 @@ private fun DocumentFileItem(
275273
overflow = TextOverflow.Ellipsis
276274
)
277275

278-
// 元数据信息
279276
if (file.metadata.chapterCount > 0 || file.metadata.totalPages != null) {
280277
Row(
281278
horizontalArrangement = Arrangement.spacedBy(8.dp),
@@ -299,7 +296,6 @@ private fun DocumentFileItem(
299296
}
300297
}
301298

302-
// 解析状态指示器
303299
when (file.metadata.parseStatus) {
304300
ParseStatus.PARSED -> {
305301
Icon(
@@ -335,21 +331,21 @@ private fun DocumentFileItem(
335331
fun buildDocumentTreeStructure(documents: List<DocumentFile>): List<DocumentTreeNode> {
336332
val rootNodes = mutableListOf<DocumentTreeNode>()
337333
val folderMap = mutableMapOf<String, DocumentFolder>()
338-
334+
339335
documents.forEach { doc ->
340336
val parts = doc.path.split("/")
341337
var currentPath = ""
342338
var parentFolder: DocumentFolder? = null
343-
339+
344340
for (i in 0 until parts.size - 1) {
345341
val part = parts[i]
346342
currentPath = if (currentPath.isEmpty()) part else "$currentPath/$part"
347-
343+
348344
var folder = folderMap[currentPath]
349345
if (folder == null) {
350346
folder = DocumentFolder(part, currentPath)
351347
folderMap[currentPath] = folder
352-
348+
353349
if (parentFolder != null) {
354350
parentFolder.children.add(folder)
355351
} else {
@@ -358,17 +354,17 @@ fun buildDocumentTreeStructure(documents: List<DocumentFile>): List<DocumentTree
358354
}
359355
parentFolder = folder
360356
}
361-
357+
362358
if (parentFolder != null) {
363359
parentFolder.children.add(doc)
364360
} else {
365361
rootNodes.add(doc)
366362
}
367363
}
368-
364+
369365
// Post-process counts
370366
rootNodes.forEach { updateFileCount(it) }
371-
367+
372368
return rootNodes
373369
}
374370

mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/document/DocumentReaderViewModel.kt

Lines changed: 33 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -110,9 +110,6 @@ class DocumentReaderViewModel(
110110
* Load a single document file from absolute path
111111
*/
112112
fun loadDocumentFromPath(absolutePath: String) {
113-
println("=== Loading Document from Path ===")
114-
println("Path: $absolutePath")
115-
116113
scope.launch {
117114
try {
118115
isLoading = true
@@ -121,16 +118,13 @@ class DocumentReaderViewModel(
121118
val fileSystem = workspace.fileSystem
122119
val name = absolutePath.substringAfterLast('/')
123120

124-
// Read file content using fileSystem
125121
val content = fileSystem.readFile(absolutePath)
126122
?: run {
127123
error = "Failed to read file: $absolutePath"
128124
println("ERROR: Failed to read file: $absolutePath")
129125
return@launch
130126
}
131127

132-
println("File read successfully, size: ${content.length} bytes")
133-
134128
val doc = DocumentFile(
135129
name = name,
136130
path = absolutePath,
@@ -146,11 +140,8 @@ class DocumentReaderViewModel(
146140
)
147141

148142
documents = listOf(doc)
149-
println("Document added to list")
150143

151144
selectDocument(doc)
152-
println("Document selected and parsing initiated")
153-
154145
} catch (e: Exception) {
155146
error = "Failed to load document: ${e.message}"
156147
println("ERROR: Failed to load document: ${e.message}")
@@ -164,36 +155,43 @@ class DocumentReaderViewModel(
164155
fun selectDocument(doc: DocumentFile) {
165156
selectedDocument = doc
166157
scope.launch {
167-
isLoading = true
168-
error = null
169-
170-
// Read file content
171-
val fileSystem = workspace.fileSystem
172-
val content = fileSystem.readFile(doc.path)
173-
?: run {
174-
error = "Failed to read file content"
175-
return@launch
176-
}
158+
try {
159+
isLoading = true
160+
error = null
161+
162+
val fileSystem = workspace.fileSystem
163+
val content = fileSystem.readFile(doc.path)
164+
?: run {
165+
error = "Failed to read file content"
166+
documentContent = null // Clear stale content
167+
return@launch
168+
}
177169

178-
documentContent = content
170+
documentContent = content
179171

180-
val parsedDoc = parserService.parse(doc, content)
181-
if (parsedDoc is DocumentFile && parsedDoc.toc.isNotEmpty()) {
182-
println("ViewModel: Received parsed TOC with ${parsedDoc.toc.size} items")
183-
val updatedDoc = doc.copy(
184-
toc = parsedDoc.toc,
185-
metadata = doc.metadata.copy(
186-
chapterCount = parsedDoc.toc.size,
187-
parseStatus = ParseStatus.PARSED
172+
val parsedDoc = parserService.parse(doc, content)
173+
if (parsedDoc is DocumentFile && parsedDoc.toc.isNotEmpty()) {
174+
println("ViewModel: Received parsed TOC with ${parsedDoc.toc.size} items")
175+
val updatedDoc = doc.copy(
176+
toc = parsedDoc.toc,
177+
metadata = doc.metadata.copy(
178+
chapterCount = parsedDoc.toc.size,
179+
parseStatus = ParseStatus.PARSED
180+
)
188181
)
189-
)
190-
selectedDocument = updatedDoc
182+
selectedDocument = updatedDoc
191183

192-
// Update in documents list
193-
documents = documents.map { if (it.path == doc.path) updatedDoc else it }
184+
// Update in documents list
185+
documents = documents.map { if (it.path == doc.path) updatedDoc else it }
186+
}
187+
} catch (e: Exception) {
188+
error = "Failed to process document: ${e.message}"
189+
documentContent = null // Clear stale content
190+
println("ERROR: Failed to process document: ${e.message}")
191+
e.printStackTrace()
192+
} finally {
193+
isLoading = false
194194
}
195-
196-
isLoading = false
197195
}
198196
}
199197

@@ -224,7 +222,7 @@ class DocumentReaderViewModel(
224222
renderer.forceStop()
225223
isGenerating = false
226224
}
227-
225+
228226
/**
229227
* Get the parser service for DocQL queries
230228
*/

mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/document/DocumentViewerPane.kt

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@ import androidx.compose.foundation.rememberScrollState
55
import androidx.compose.foundation.verticalScroll
66
import androidx.compose.material3.*
77
import androidx.compose.runtime.Composable
8+
import androidx.compose.runtime.remember
89
import androidx.compose.ui.Alignment
910
import androidx.compose.ui.Modifier
1011
import androidx.compose.ui.text.font.FontWeight
1112
import androidx.compose.ui.unit.dp
1213
import cc.unitmesh.devins.document.DocumentFile
1314
import cc.unitmesh.devins.ui.compose.icons.AutoDevComposeIcons
14-
import cc.unitmesh.devins.ui.compose.sketch.MarkdownSketchRenderer
1515

1616
/**
1717
* 文档查看面板 - 中间上部
@@ -39,7 +39,7 @@ fun DocumentViewerPane(
3939
fontWeight = FontWeight.Bold,
4040
modifier = Modifier.padding(bottom = 8.dp)
4141
)
42-
42+
4343
// 文档元数据栏
4444
Row(
4545
modifier = Modifier
@@ -52,31 +52,36 @@ fun DocumentViewerPane(
5252
icon = AutoDevComposeIcons.Schedule,
5353
text = "Last modified: ${formatDate(document.metadata.lastModified)}"
5454
)
55-
55+
5656
if (document.metadata.fileSize > 0) {
5757
MetadataItem(
5858
icon = AutoDevComposeIcons.Description,
5959
text = formatFileSize(document.metadata.fileSize)
6060
)
6161
}
6262
}
63-
63+
6464
HorizontalDivider(modifier = Modifier.padding(bottom = 16.dp))
65-
65+
6666
// 文档内容
6767
Box(modifier = Modifier.fillMaxSize()) {
6868
if (isLoading) {
6969
CircularProgressIndicator(
7070
modifier = Modifier.align(Alignment.Center)
7171
)
7272
} else if (content != null) {
73+
val sanitizedContent = remember(content) {
74+
content.trim()
75+
}
76+
7377
Column(
7478
modifier = Modifier
7579
.fillMaxSize()
7680
.verticalScroll(rememberScrollState())
7781
) {
78-
MarkdownSketchRenderer.RenderMarkdown(
79-
markdown = content,
82+
Text(
83+
text = sanitizedContent,
84+
style = MaterialTheme.typography.bodySmall,
8085
modifier = Modifier.fillMaxWidth()
8186
)
8287
}
@@ -141,7 +146,7 @@ private fun MetadataItem(
141146
// 简单的日期格式化 (实际项目中应使用 DateTimeFormatter)
142147
private fun formatDate(timestamp: Long): String {
143148
// 这里只是一个简单的占位符,实际应该用 kotlinx-datetime
144-
return "Recently"
149+
return "Recently"
145150
}
146151

147152
// 文件大小格式化

0 commit comments

Comments
 (0)