Skip to content

Commit 34a6dd3

Browse files
committed
refactor: improve robustness with null safety and path validation
- Replace runBlocking with tryEmit and add buffer capacity in FimCache - Add project path validation and canonical path checks in SharedChatPane - Improve error handling with try-catch and null checks across LSP, chat, and status bar - Enhance URL validation for external links and scheme handlers - Simplify synchronization and state management in browser lifecycle and message queue - Add safe HTML escaping and atomic state updates in status bar widget - Fix LSP process startup with better retries and logging
1 parent b7e47f1 commit 34a6dd3

File tree

19 files changed

+457
-400
lines changed

19 files changed

+457
-400
lines changed

src/main/kotlin/com/smallcloud/refactai/FimCache.kt

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,26 +4,22 @@ import com.google.gson.Gson
44
import com.smallcloud.refactai.panes.sharedchat.Events
55
import kotlinx.coroutines.flow.*
66

7-
import kotlinx.coroutines.runBlocking
8-
97

108
object FimCache {
11-
private val _events = MutableSharedFlow<Events.Fim.FimDebugPayload>();
12-
val events = _events.asSharedFlow();
9+
private val _events = MutableSharedFlow<Events.Fim.FimDebugPayload>(extraBufferCapacity = 64)
10+
val events = _events.asSharedFlow()
1311

1412
suspend fun subscribe(block: (Events.Fim.FimDebugPayload) -> Unit) {
1513
events.filterIsInstance<Events.Fim.FimDebugPayload>().collectLatest {
1614
block(it)
1715
}
1816
}
1917

20-
2118
fun emit(data: Events.Fim.FimDebugPayload) {
22-
runBlocking {
23-
_events.emit(data)
24-
}
19+
_events.tryEmit(data)
2520
}
2621

22+
@Volatile
2723
var last: Events.Fim.FimDebugPayload? = null
2824

2925
fun maybeSendFimData(res: String) {

src/main/kotlin/com/smallcloud/refactai/PluginState.kt

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,30 +20,31 @@ interface ExtraInfoChangedNotifier {
2020
class PluginState : Disposable {
2121
private val messageBus: MessageBus = ApplicationManager.getApplication().messageBus
2222

23-
var tooltipMessage: String? = null
23+
var tooltipMessage: String?
2424
get() = AppSettingsState.instance.tooltipMessage
2525
set(newMsg) {
2626
if (AppSettingsState.instance.tooltipMessage == newMsg) return
27+
AppSettingsState.instance.tooltipMessage = newMsg
2728
messageBus
2829
.syncPublisher(ExtraInfoChangedNotifier.TOPIC)
29-
.tooltipMessageChanged(field)
30+
.tooltipMessageChanged(newMsg)
3031
}
3132

32-
var inferenceMessage: String? = null
33+
var inferenceMessage: String?
3334
get() = AppSettingsState.instance.inferenceMessage
3435
set(newMsg) {
35-
if (field != newMsg) {
36-
field = newMsg
37-
messageBus
38-
.syncPublisher(ExtraInfoChangedNotifier.TOPIC)
39-
.inferenceMessageChanged(field)
40-
}
36+
if (AppSettingsState.instance.inferenceMessage == newMsg) return
37+
AppSettingsState.instance.inferenceMessage = newMsg
38+
messageBus
39+
.syncPublisher(ExtraInfoChangedNotifier.TOPIC)
40+
.inferenceMessageChanged(newMsg)
4141
}
4242

4343
var loginMessage: String?
4444
get() = AppSettingsState.instance.loginMessage
4545
set(newMsg) {
46-
if (loginMessage == newMsg) return
46+
if (AppSettingsState.instance.loginMessage == newMsg) return
47+
AppSettingsState.instance.loginMessage = newMsg
4748
messageBus
4849
.syncPublisher(ExtraInfoChangedNotifier.TOPIC)
4950
.loginMessageChanged(newMsg)

src/main/kotlin/com/smallcloud/refactai/code_lens/CodeLensAction.kt

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -91,11 +91,8 @@ class CodeLensAction(
9191
if (messages.isEmpty() && isActionRunning.compareAndSet(false, true)) {
9292
ApplicationManager.getApplication().invokeLater {
9393
try {
94-
val pos1 = LogicalPosition(line1, 0)
95-
val pos2 = LogicalPosition(line2, editor.document.getLineEndOffset(line2))
96-
97-
val intendedStart = editor.logicalPositionToOffset(pos1)
98-
val intendedEnd = editor.logicalPositionToOffset(pos2)
94+
val intendedStart = editor.document.getLineStartOffset(line1)
95+
val intendedEnd = editor.document.getLineEndOffset(line2)
9996
editor.selectionModel.setSelection(intendedStart, intendedEnd)
10097
} finally {
10198
isActionRunning.set(false)

src/main/kotlin/com/smallcloud/refactai/code_lens/RefactCodeVisionProvider.kt

Lines changed: 58 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -52,41 +52,67 @@ class RefactCodeVisionProvider(
5252
private fun getCodeLens(editor: Editor): List<CodeLen> {
5353
val codeLensStr = lspGetCodeLens(editor)
5454
val gson = Gson()
55-
val codeLensJson = gson.fromJson(codeLensStr, JsonObject::class.java)
55+
val codeLensJson = try {
56+
gson.fromJson(codeLensStr, JsonObject::class.java)
57+
} catch (_: Exception) {
58+
return emptyList()
59+
} ?: return emptyList()
60+
5661
val resCodeLenses = mutableListOf<CodeLen>()
57-
if (customization.has("code_lens")) {
58-
val allCodeLenses = customization.get("code_lens").asJsonObject
59-
if (codeLensJson.has("code_lens")) {
60-
val codeLenses = codeLensJson.get("code_lens")!!.asJsonArray
61-
for (codeLens in codeLenses) {
62-
val line1 = max(codeLens.asJsonObject.get("line1").asInt - 1, 0)
63-
val line2 = max(codeLens.asJsonObject.get("line2").asInt - 1, 0)
64-
val range = runReadAction {
65-
return@runReadAction TextRange(
66-
editor.logicalPositionToOffset(LogicalPosition(line1, 0)),
67-
editor.document.getLineEndOffset(line2)
68-
)
69-
}
70-
val value = allCodeLenses.get(commandKey).asJsonObject
71-
val msgs = value.asJsonObject.get("messages").asJsonArray.map {
72-
gson.fromJson(it.asJsonObject, ChatMessage::class.java)
62+
if (!customization.has("code_lens")) return resCodeLenses
63+
64+
val allCodeLenses = customization.get("code_lens")
65+
if (allCodeLenses == null || !allCodeLenses.isJsonObject) return resCodeLenses
66+
67+
if (!codeLensJson.has("code_lens")) return resCodeLenses
68+
val codeLenses = codeLensJson.get("code_lens")
69+
if (codeLenses == null || !codeLenses.isJsonArray) return resCodeLenses
70+
71+
val lineCount = runReadAction { editor.document.lineCount }
72+
if (lineCount == 0) return resCodeLenses
73+
74+
for (codeLens in codeLenses.asJsonArray) {
75+
try {
76+
val obj = codeLens.asJsonObject
77+
var line1 = max(obj.get("line1")?.asInt?.minus(1) ?: continue, 0).coerceAtMost(lineCount - 1)
78+
var line2 = max(obj.get("line2")?.asInt?.minus(1) ?: continue, 0).coerceAtMost(lineCount - 1)
79+
if (line2 < line1) {
80+
val tmp = line1
81+
line1 = line2
82+
line2 = tmp
83+
}
84+
85+
val value = allCodeLenses.asJsonObject.get(commandKey)
86+
if (value == null || !value.isJsonObject) continue
87+
88+
val range = runReadAction {
89+
val startOffset = editor.document.getLineStartOffset(line1)
90+
val endOffset = editor.document.getLineEndOffset(line2)
91+
TextRange(startOffset, endOffset)
92+
}
93+
94+
val messagesJson = value.asJsonObject.get("messages")
95+
val msgs = if (messagesJson != null && messagesJson.isJsonArray) {
96+
messagesJson.asJsonArray.mapNotNull {
97+
try { gson.fromJson(it.asJsonObject, ChatMessage::class.java) } catch (_: Exception) { null }
7398
}.toTypedArray()
74-
val userMsg = msgs.find { it.role == "user" }
75-
76-
val sendImmediately = value.asJsonObject.get("auto_submit").asBoolean
77-
val openNewTab = value.asJsonObject.get("new_tab")?.asBoolean ?: true
78-
79-
val isValidCodeLen = msgs.isEmpty() || userMsg != null
80-
if (isValidCodeLen) {
81-
resCodeLenses.add(
82-
CodeLen(
83-
range,
84-
value.asJsonObject.get("label").asString,
85-
CodeLensAction(editor, line1, line2, msgs, sendImmediately, openNewTab)
86-
)
87-
)
88-
}
99+
} else {
100+
emptyArray()
101+
}
102+
val userMsg = msgs.find { it.role == "user" }
103+
104+
val sendImmediately = value.asJsonObject.get("auto_submit")?.asBoolean ?: false
105+
val openNewTab = value.asJsonObject.get("new_tab")?.asBoolean ?: true
106+
val label = value.asJsonObject.get("label")?.asString ?: continue
107+
108+
val isValidCodeLen = msgs.isEmpty() || userMsg != null
109+
if (isValidCodeLen) {
110+
resCodeLenses.add(
111+
CodeLen(range, label, CodeLensAction(editor, line1, line2, msgs, sendImmediately, openNewTab))
112+
)
89113
}
114+
} catch (_: Exception) {
115+
continue
90116
}
91117
}
92118

src/main/kotlin/com/smallcloud/refactai/codecompletion/RefactAICompletionProvider.kt

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ import com.smallcloud.refactai.struct.SMCRequest
3535
import com.smallcloud.refactai.utils.getExtension
3636
import dev.gitlive.difflib.DiffUtils
3737
import dev.gitlive.difflib.patch.DeltaType
38-
import kotlinx.coroutines.CoroutineScope
3938
import kotlinx.coroutines.Dispatchers
4039
import kotlinx.coroutines.channels.awaitClose
4140
import kotlinx.coroutines.delay
@@ -74,7 +73,7 @@ private class Default : InlineCompletionSuggestionUpdateManager.Adapter {
7473

7574
private fun truncateFirstSymbol(elements: List<InlineCompletionElement>): List<InlineCompletionElement>? {
7675
val newFirstElementIndex = elements.indexOfFirst { it.text.isNotEmpty() }
77-
check(newFirstElementIndex >= 0)
76+
if (newFirstElementIndex < 0) return null
7877
val firstElement = elements[newFirstElementIndex]
7978
val manipulator = InlineCompletionElementManipulator.getApplicable(firstElement) ?: return null
8079
val newFirstElement = manipulator.truncateFirstSymbol(firstElement)
@@ -195,7 +194,8 @@ class RefactAICompletionProvider : DebouncedInlineCompletionProvider() {
195194

196195
if (!state.isValid()) return null
197196
val stat = UsageStatistic(scope = "completion", extension = getExtension(fileName))
198-
val baseUrl = getInstance(editor.project!!)?.url!!
197+
val project = editor.project ?: return null
198+
val baseUrl = getInstance(project)?.url ?: return null
199199
val httpRequest = RequestCreator.create(
200200
fileName, text, logicalPos.line, pos,
201201
stat,
@@ -226,20 +226,20 @@ class RefactAICompletionProvider : DebouncedInlineCompletionProvider() {
226226
InferenceGlobalContext.status = ConnectionStatus.CONNECTED
227227
InferenceGlobalContext.lastErrorMsg = null
228228
}) { prediction ->
229-
val choice = prediction.choices.first()
229+
val choice = prediction.choices.firstOrNull() ?: return@streamedInferenceFetch
230230
if (lastRequestId != prediction.requestId) {
231231
return@streamedInferenceFetch
232232
}
233-
val completion = Completion(
234-
context.request.body.inputs.sources.values.toList().first(),
233+
val completion = Completion(
234+
context.request.body.inputs.sources.values.toList().firstOrNull() ?: return@streamedInferenceFetch,
235235
offset = context.editorState.offset,
236236
multiline = context.request.body.inputs.multiline,
237237
createdTs = prediction.created,
238238
isFromCache = prediction.cached,
239239
snippetTelemetryId = prediction.snippetTelemetryId
240240
)
241241
completion.updateCompletion(choice.delta)
242-
CoroutineScope(Dispatchers.Default).launch {
242+
launch(Dispatchers.Default) {
243243
val elems = if (completion.multiline) {
244244
getMultilineElements(completion, context.editorState)
245245
} else {

src/main/kotlin/com/smallcloud/refactai/io/InferenceGlobalContext.kt

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -88,11 +88,8 @@ class InferenceGlobalContext : Disposable {
8888

8989
val deploymentMode: DeploymentMode
9090
get() {
91-
if (AppSettingsState.userInferenceUri == null) {
92-
return DeploymentMode.CLOUD
93-
}
94-
95-
return when(AppSettingsState.userInferenceUri!!.lowercase()) {
91+
val uri = AppSettingsState.userInferenceUri ?: return DeploymentMode.CLOUD
92+
return when(uri.lowercase()) {
9693
"hf" -> DeploymentMode.HF
9794
"refact" -> DeploymentMode.CLOUD
9895
else -> DeploymentMode.SELF_HOSTED

src/main/kotlin/com/smallcloud/refactai/listeners/LSPDocumentListener.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ class LSPDocumentListener : BulkAwareDocumentListener, Disposable {
1717
val editor = getActiveEditor(event.document) ?: return
1818
val vFile = getVirtualFile(editor) ?: return
1919
if (!vFile.exists()) return
20-
val project = editor.project!!
20+
val project = editor.project ?: return
2121

2222
lspDocumentDidChanged(project, vFile.url, editor.document.text)
2323
}

src/main/kotlin/com/smallcloud/refactai/lsp/LSPConfig.kt

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,23 @@ data class LSPConfig(
2626
params.add("--http-port")
2727
params.add("$port")
2828
}
29-
if (apiKey != null) {
29+
apiKey?.let {
3030
params.add("--api-key")
31-
params.add("$apiKey")
31+
params.add(it)
3232
}
33+
return params + toCommonArgs()
34+
}
35+
36+
fun toSafeLogString(): String {
37+
val safe = mutableListOf<String>()
38+
address?.let { safe.add("--address-url $it") }
39+
port?.let { safe.add("--http-port $it") }
40+
if (apiKey != null) safe.add("--api-key ***")
41+
return (safe + toCommonArgs()).joinToString(" ")
42+
}
43+
44+
private fun toCommonArgs(): List<String> {
45+
val params = mutableListOf<String>()
3346
if (clientVersion != null) {
3447
params.add("--enduser-client-version")
3548
params.add("$clientVersion")

src/main/kotlin/com/smallcloud/refactai/lsp/LSPHelper.kt

Lines changed: 31 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,14 @@ import com.smallcloud.refactai.lsp.LSPProcessHolder.Companion.getInstance as get
2626
fun findRoots(paths: List<String>): List<String> {
2727
val sortedPaths = paths.map { Paths.get(it).normalize() }.sortedBy { it.nameCount }
2828

29-
val roots = mutableSetOf<String>()
29+
val roots = mutableListOf<java.nio.file.Path>()
3030

3131
for (path in sortedPaths) {
32-
val pathStr = path.toString()
33-
if (roots.none { pathStr.startsWith("$it/") || pathStr == it }) {
34-
roots.add(pathStr)
32+
if (roots.none { path.startsWith(it) }) {
33+
roots.add(path)
3534
}
3635
}
37-
return roots.toList()
36+
return roots.map { it.toString() }
3837
}
3938

4039
fun lspProjectInitialize(lsp: LSPProcessHolder, project: Project) {
@@ -124,25 +123,27 @@ fun lspSetActiveDocument(editor: Editor) {
124123

125124

126125
fun lspGetCodeLens(editor: Editor): String {
127-
val project = editor.project!!
126+
val project = editor.project ?: return ""
127+
val virtualFile = editor.virtualFile ?: return ""
128128
val url = getLSPProcessHolder(project)?.url?.resolve("/v1/code-lens") ?: return ""
129129
val data = Gson().toJson(
130130
mapOf(
131-
"uri" to editor.virtualFile.url,
131+
"uri" to virtualFile.url,
132132
)
133133
)
134134

135-
InferenceGlobalContext.connection.post(url, data, dataReceiveEnded = {
136-
InferenceGlobalContext.status = ConnectionStatus.CONNECTED
137-
InferenceGlobalContext.lastErrorMsg = null
138-
}, failedDataReceiveEnded = {
139-
InferenceGlobalContext.status = ConnectionStatus.ERROR
140-
if (it != null) {
141-
InferenceGlobalContext.lastErrorMsg = it.message
142-
}
143-
}).let {
144-
val res = it.get()!!.get() as String
145-
return res
135+
return try {
136+
InferenceGlobalContext.connection.post(url, data, dataReceiveEnded = {
137+
InferenceGlobalContext.status = ConnectionStatus.CONNECTED
138+
InferenceGlobalContext.lastErrorMsg = null
139+
}, failedDataReceiveEnded = {
140+
InferenceGlobalContext.status = ConnectionStatus.ERROR
141+
if (it != null) {
142+
InferenceGlobalContext.lastErrorMsg = it.message
143+
}
144+
}).get()?.get() as? String ?: ""
145+
} catch (e: Exception) {
146+
""
146147
}
147148
}
148149

@@ -160,16 +161,17 @@ fun lspGetCommitMessage(project: Project, diff: String, currentMessage: String):
160161
}
161162
val data = Gson().toJson(requestBody)
162163

163-
InferenceGlobalContext.connection.post(url, data, dataReceiveEnded = {
164-
InferenceGlobalContext.status = ConnectionStatus.CONNECTED
165-
InferenceGlobalContext.lastErrorMsg = null
166-
}, failedDataReceiveEnded = {
167-
InferenceGlobalContext.status = ConnectionStatus.ERROR
168-
if (it != null) {
169-
InferenceGlobalContext.lastErrorMsg = it.message
170-
}
171-
}).let {
172-
val res = it.get()!!.get() as String
173-
return res
164+
return try {
165+
InferenceGlobalContext.connection.post(url, data, dataReceiveEnded = {
166+
InferenceGlobalContext.status = ConnectionStatus.CONNECTED
167+
InferenceGlobalContext.lastErrorMsg = null
168+
}, failedDataReceiveEnded = {
169+
InferenceGlobalContext.status = ConnectionStatus.ERROR
170+
if (it != null) {
171+
InferenceGlobalContext.lastErrorMsg = it.message
172+
}
173+
}).get()?.get() as? String ?: ""
174+
} catch (e: Exception) {
175+
""
174176
}
175177
}

0 commit comments

Comments
 (0)