Skip to content

Commit bb3c6f3

Browse files
committed
Refine previewer recovery per review feedback
1 parent aafcb94 commit bb3c6f3

File tree

4 files changed

+80
-80
lines changed

4 files changed

+80
-80
lines changed

src/rider/main/kotlin/me/fornever/avaloniarider/idea/editor/AvaloniaPreviewEditorBase.kt

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import com.intellij.codeHighlighting.BackgroundEditorHighlighter
44
import com.intellij.execution.filters.TextConsoleBuilderFactory
55
import com.intellij.openapi.actionSystem.*
66
import com.intellij.openapi.application.EDT
7+
import com.intellij.openapi.editor.Document
8+
import com.intellij.openapi.fileEditor.FileDocumentManager
79
import com.intellij.openapi.fileEditor.FileEditorLocation
810
import com.intellij.openapi.fileEditor.FileEditorState
911
import com.intellij.openapi.observable.properties.AtomicProperty
@@ -16,6 +18,7 @@ import com.intellij.openapi.wm.WindowManager
1618
import com.intellij.ui.dsl.builder.Align
1719
import com.intellij.ui.dsl.builder.bindText
1820
import com.intellij.ui.dsl.builder.panel
21+
import com.intellij.util.application
1922
import com.intellij.util.ui.UIUtil
2023
import com.jetbrains.rd.util.lifetime.Lifetime
2124
import com.jetbrains.rd.util.lifetime.LifetimeDefinition
@@ -143,7 +146,16 @@ abstract class AvaloniaPreviewEditorBase(
143146

144147
private val assemblySelectorAction = RunnableAssemblySelectorAction(lifetime, project, currentFile)
145148
private val selectedProjectPath = assemblySelectorAction.selectedProjectPath
146-
protected val sessionController = AvaloniaPreviewerSessionController(project, lifetime, consoleView, file, selectedProjectPath)
149+
private val baseDocument: Document? =
150+
application.runReadAction<Document?> { FileDocumentManager.getInstance().getDocument(file) }
151+
protected val sessionController = AvaloniaPreviewerSessionController(
152+
project,
153+
lifetime,
154+
consoleView,
155+
file,
156+
selectedProjectPath,
157+
baseDocument
158+
)
147159
init {
148160
lifetime.launch(Dispatchers.EDT) {
149161
sessionController.status.nextValue { it == AvaloniaPreviewerSessionController.Status.Working }

src/rider/main/kotlin/me/fornever/avaloniarider/idea/editor/actions/RunnableAssemblySelectorAction.kt

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -238,12 +238,7 @@ class RunnableAssemblySelectorAction(
238238
val targetFileProjectEntity = xamlFile.getProjectContainingFile(lifetime, project)
239239
if (targetFileProjectEntity == null) {
240240
logger.warn("Unable to determine project containing file $xamlFile; falling back to all runnable projects")
241-
return filteredProjectList
242-
.filter { !isWebAssemblyProject(it) }
243-
.sortedWith(compareBy(
244-
{ !it.name.endsWith(".Desktop", ignoreCase = true) },
245-
{ it.name }
246-
))
241+
return filteredProjectList.asSequence().preferDesktopProjects()
247242
}
248243
val targetFileProjectPath = targetFileProjectEntity.url!!.toPath()
249244
val runnableProjectPaths = filteredProjectList
@@ -283,14 +278,19 @@ class RunnableAssemblySelectorAction(
283278
}
284279
runnableProject
285280
}
286-
.filter { !isWebAssemblyProject(it) }
281+
.asSequence()
282+
.preferDesktopProjects()
283+
return selectableRunnableProjects
284+
}
285+
286+
private fun Sequence<RunnableProject>.preferDesktopProjects(): List<RunnableProject> =
287+
filter { !isWebAssemblyProject(it) }
287288
.sortedWith(compareBy(
288289
// Sort Desktop projects first, then others alphabetically
289290
{ !it.name.endsWith(".Desktop", ignoreCase = true) },
290291
{ it.name }
291292
))
292-
return selectableRunnableProjects
293-
}
293+
.toList()
294294

295295
private fun isWebAssemblyProject(project: RunnableProject): Boolean {
296296
return project.projectOutputs.all { it.tfm?.presentableName.orEmpty().endsWith("-browser") }

src/rider/main/kotlin/me/fornever/avaloniarider/previewer/AvaloniaPreviewerSessionController.kt

Lines changed: 50 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,11 @@ import com.intellij.openapi.application.EDT
55
import com.intellij.openapi.diagnostic.Logger
66
import com.intellij.openapi.diagnostic.debug
77
import com.intellij.openapi.editor.Document
8-
import com.intellij.openapi.fileEditor.FileDocumentManager
98
import com.intellij.openapi.project.Project
10-
import com.intellij.openapi.rd.createNestedDisposable
9+
import com.intellij.openapi.util.io.FileUtil
1110
import com.intellij.openapi.vfs.VirtualFile
1211
import com.intellij.platform.backend.workspace.WorkspaceModel
1312
import com.intellij.ui.scale.JBUIScale
14-
import com.intellij.util.Alarm
1513
import com.intellij.util.application
1614
import com.jetbrains.rd.util.lifetime.Lifetime
1715
import com.jetbrains.rd.util.lifetime.LifetimeDefinition
@@ -24,6 +22,7 @@ import com.jetbrains.rd.util.throttleLast
2422
import com.jetbrains.rider.build.BuildHost
2523
import com.jetbrains.rider.model.riderSolutionLifecycle
2624
import com.jetbrains.rider.projectView.solution
25+
import com.intellij.workspaceModel.ide.toPath
2726
import com.jetbrains.rider.projectView.workspace.ProjectModelEntity
2827
import com.jetbrains.rider.projectView.workspace.containingProjectEntity
2928
import com.jetbrains.rider.projectView.workspace.getProjectModelEntities
@@ -32,6 +31,11 @@ import com.jetbrains.rider.ui.components.utils.documentChanged
3231
import kotlinx.coroutines.CancellationException
3332
import kotlinx.coroutines.CompletableDeferred
3433
import kotlinx.coroutines.Dispatchers
34+
import kotlinx.coroutines.Job
35+
import kotlinx.coroutines.channels.BufferOverflow
36+
import kotlinx.coroutines.delay
37+
import kotlinx.coroutines.flow.MutableSharedFlow
38+
import kotlinx.coroutines.flow.collectLatest
3539
import kotlinx.coroutines.withContext
3640
import me.fornever.avaloniarider.AvaloniaRiderBundle
3741
import me.fornever.avaloniarider.controlmessages.AvaloniaInputEventMessage
@@ -61,7 +65,8 @@ class AvaloniaPreviewerSessionController(
6165
outerLifetime: Lifetime,
6266
private val consoleView: ConsoleView?,
6367
private val xamlFile: VirtualFile,
64-
projectFilePathProperty: IOptPropertyView<Path>) {
68+
private val projectFilePathProperty: IOptPropertyView<Path>,
69+
private val baseDocument: Document?) {
6570
companion object {
6671
private val logger = Logger.getInstance(AvaloniaPreviewerSessionController::class.java)
6772

@@ -133,19 +138,11 @@ class AvaloniaPreviewerSessionController(
133138
@Volatile
134139
private var lastKnownProjectRelativePath: String? = null
135140
@Volatile
136-
private var lastProjectFilePath: Path? = null
137-
@Volatile
138141
private var pendingRestart = false
139142

140143
private val sessionLifetimeSource = SequentialLifetimes(controllerLifetime)
141144
private var currentSessionLifetime: LifetimeDefinition? = null
142-
private val restartLifetime = controllerLifetime.createNestedDisposable(
143-
"AvaloniaPreviewerSessionController.restartLifetime"
144-
)
145-
private val restartAlarm = Alarm(restartLifetime)
146-
147-
private val baseDocument: Document? =
148-
application.runReadAction<Document?> { FileDocumentManager.getInstance().getDocument(xamlFile) }
145+
private var restartJob: Job? = null
149146

150147
init {
151148
val isBuildingProperty = BuildHost.getInstance(project).building
@@ -181,8 +178,7 @@ class AvaloniaPreviewerSessionController(
181178
?.advise(controllerLifetime) {
182179
if (session != null) return@advise
183180
if (!pendingRestart) return@advise
184-
val projectPath = lastProjectFilePath ?: return@advise
185-
scheduleRestart(projectPath)
181+
scheduleRestart()
186182
}
187183
}
188184

@@ -235,44 +231,19 @@ class AvaloniaPreviewerSessionController(
235231

236232
sendClientSupportedPixelFormat()
237233

238-
var document: Document? = null
239-
application.runReadAction {
240-
document = FileDocumentManager.getInstance().getDocument(xamlFile)
241-
}
234+
val document = baseDocument
242235
if (document == null) {
243236
logger.warn("Unable to obtain document for $xamlFile")
244237
return@advise
245238
}
246-
val currentDocument = document!!
247-
248-
val pathRetryDisposable = lifetime.createNestedDisposable(
249-
"AvaloniaPreviewerSessionController.projectPathRetry"
250-
)
251-
val pathRetryAlarm = Alarm(pathRetryDisposable)
252-
var pendingPathRetry = false
253-
254-
fun cancelScheduledRetry() {
255-
if (!pendingPathRetry) return
256-
pendingPathRetry = false
257-
pathRetryAlarm.cancelAllRequests()
258-
}
259-
260-
fun computeDocumentPathInProject(): String? {
261-
val projectModelItems = workspaceModel.getProjectModelEntities(xamlFile, project)
262-
val item = projectModelItems.firstOrNull()
263-
return item?.projectRelativeVirtualPath?.let { "/$it" }
264-
}
265-
266-
lateinit var schedulePathRetry: () -> Unit
239+
val documentUpdates = MutableSharedFlow<Unit>(replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
267240

268-
fun dispatchXamlUpdate() {
269-
application.runReadAction {
270-
val projectPath = computeDocumentPathInProject()
271-
if (projectPath == null) {
272-
logger.debug { "Project relative path is not yet available for $xamlFile" }
241+
suspend fun dispatchXamlUpdate() {
242+
while (lifetime.isAlive) {
243+
val (text, projectRelativePath) = application.runReadAction<Pair<String, String?>> {
244+
document.text to computeDocumentPathInProject(projectFilePathProperty.valueOrNull)
273245
}
274-
275-
val effectivePath = projectPath ?: lastKnownProjectRelativePath
246+
val effectivePath = projectRelativePath ?: lastKnownProjectRelativePath
276247
if (effectivePath == null) {
277248
val message = "Skipping XAML update for ${xamlFile.name}: project path is unknown"
278249
if (lastKnownProjectRelativePath == null) {
@@ -281,34 +252,29 @@ class AvaloniaPreviewerSessionController(
281252
logger.debug { message }
282253
}
283254
inFlightUpdate.value = false
284-
schedulePathRetry()
285-
return@runReadAction
255+
delay(projectPathRetryDelay.toMillis())
256+
continue
286257
}
287258

288259
lastKnownProjectRelativePath = effectivePath
289-
cancelScheduledRetry()
290-
291260
inFlightUpdate.value = true
292-
sendXamlUpdate(currentDocument.text, effectivePath)
261+
sendXamlUpdate(text, effectivePath)
262+
break
293263
}
294264
}
295265

296-
schedulePathRetry = retry@{
297-
if (pendingPathRetry) return@retry
298-
pendingPathRetry = true
299-
pathRetryAlarm.addRequest({
300-
pendingPathRetry = false
301-
if (!lifetime.isAlive) return@addRequest
266+
lifetime.launch {
267+
documentUpdates.collectLatest {
302268
dispatchXamlUpdate()
303-
}, projectPathRetryDelay.toMillis().toInt())
269+
}
304270
}
305271

306-
currentDocument.documentChanged()
272+
document.documentChanged()
307273
.throttleLast(xamlEditThrottling, SwingScheduler)
308274
.advise(lifetime) {
309-
dispatchXamlUpdate()
275+
documentUpdates.tryEmit(Unit)
310276
}
311-
dispatchXamlUpdate()
277+
documentUpdates.tryEmit(Unit)
312278
}
313279

314280
htmlTransportStarted.flowInto(lifetime, htmlTransportStartedSignal)
@@ -401,7 +367,8 @@ class AvaloniaPreviewerSessionController(
401367
val processResult = processJob.await()
402368
sessionJob.await()
403369
val exitCode = processResult.exitCode
404-
if (exitCode != null && exitCode != 0) {
370+
// Only treat as error if lifetime is still alive; otherwise process was intentionally terminated
371+
if (lifetime.isAlive && exitCode != null && exitCode != 0) {
405372
throw AvaloniaPreviewerExecutionException(exitCode, processResult.outputSnippet)
406373
}
407374
}
@@ -411,7 +378,6 @@ class AvaloniaPreviewerSessionController(
411378
force: Boolean = false,
412379
executionMode: ProcessExecutionMode = ProcessExecutionMode.Run
413380
) {
414-
lastProjectFilePath = projectFilePath
415381
if (status.value == Status.Suspended && !force) return
416382

417383
lastKnownProjectRelativePath = null
@@ -469,12 +435,27 @@ class AvaloniaPreviewerSessionController(
469435
session?.sendInputEventMessage(event)
470436
}
471437

472-
private fun scheduleRestart(projectFilePath: Path) {
473-
restartAlarm.cancelAllRequests()
474-
restartAlarm.addRequest({
475-
if (session != null) return@addRequest
438+
private fun scheduleRestart() {
439+
val projectFilePath = projectFilePathProperty.valueOrNull ?: return
440+
restartJob?.cancel()
441+
restartJob = controllerLifetime.launch {
442+
delay(projectPathRetryDelay.toMillis())
443+
if (session != null) return@launch
476444
logger.info("Restarting previewer for $xamlFile after document change")
477445
start(projectFilePath, force = true)
478-
}, projectPathRetryDelay.toMillis().toInt())
446+
}
447+
}
448+
449+
private fun computeDocumentPathInProject(projectFilePath: Path?): String? {
450+
val projectModelItems = workspaceModel.getProjectModelEntities(xamlFile, project)
451+
val candidate = projectFilePath?.let { desiredPath ->
452+
projectModelItems.firstOrNull { entity ->
453+
val containingProject = entity.containingProjectEntity() ?: return@firstOrNull false
454+
val containingPath = containingProject.url?.toPath() ?: return@firstOrNull false
455+
FileUtil.pathsEqual(containingPath.toString(), desiredPath.toString())
456+
}
457+
} ?: projectModelItems.firstOrNull()
458+
459+
return candidate?.projectRelativeVirtualPath?.let { "/$it" }
479460
}
480461
}

src/test/kotlin/me/fornever/avaloniarider/test/cases/PreviewTests.kt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package me.fornever.avaloniarider.test.cases
22

3+
import com.intellij.openapi.editor.Document
4+
import com.intellij.openapi.fileEditor.FileDocumentManager
35
import com.intellij.openapi.util.registry.Registry
6+
import com.intellij.util.application
47
import com.jetbrains.rd.platform.diagnostics.RdLogTraceScenarios
58
import com.jetbrains.rd.util.lifetime.Lifetime
69
import com.jetbrains.rd.util.reactive.OptProperty
@@ -74,12 +77,16 @@ class PreviewTests : PerTestSolutionTestBase() {
7477
Lifetime.using { lt ->
7578
// not init the property, so that the session doesn't start before we handle the frame
7679
val projectFilePathProperty = OptProperty<Path>()
80+
val document = application.runReadAction<Document?> {
81+
FileDocumentManager.getInstance().getDocument(mainWindowFile)
82+
}
7783
AvaloniaPreviewerSessionController(
7884
project,
7985
lt,
8086
consoleView = null,
8187
mainWindowFile,
82-
projectFilePathProperty
88+
projectFilePathProperty,
89+
document
8390
).apply {
8491
frame.advise(lt) {
8592
frameMsg = it

0 commit comments

Comments
 (0)