@@ -5,13 +5,11 @@ import com.intellij.openapi.application.EDT
55import com.intellij.openapi.diagnostic.Logger
66import com.intellij.openapi.diagnostic.debug
77import com.intellij.openapi.editor.Document
8- import com.intellij.openapi.fileEditor.FileDocumentManager
98import com.intellij.openapi.project.Project
10- import com.intellij.openapi.rd.createNestedDisposable
9+ import com.intellij.openapi.util.io.FileUtil
1110import com.intellij.openapi.vfs.VirtualFile
1211import com.intellij.platform.backend.workspace.WorkspaceModel
1312import com.intellij.ui.scale.JBUIScale
14- import com.intellij.util.Alarm
1513import com.intellij.util.application
1614import com.jetbrains.rd.util.lifetime.Lifetime
1715import com.jetbrains.rd.util.lifetime.LifetimeDefinition
@@ -24,6 +22,7 @@ import com.jetbrains.rd.util.throttleLast
2422import com.jetbrains.rider.build.BuildHost
2523import com.jetbrains.rider.model.riderSolutionLifecycle
2624import com.jetbrains.rider.projectView.solution
25+ import com.intellij.workspaceModel.ide.toPath
2726import com.jetbrains.rider.projectView.workspace.ProjectModelEntity
2827import com.jetbrains.rider.projectView.workspace.containingProjectEntity
2928import com.jetbrains.rider.projectView.workspace.getProjectModelEntities
@@ -32,6 +31,11 @@ import com.jetbrains.rider.ui.components.utils.documentChanged
3231import kotlinx.coroutines.CancellationException
3332import kotlinx.coroutines.CompletableDeferred
3433import 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
3539import kotlinx.coroutines.withContext
3640import me.fornever.avaloniarider.AvaloniaRiderBundle
3741import 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}
0 commit comments