[lst] implement isTrackedFile for platform trackers
[idea/community.git] / platform / vcs-impl / src / com / intellij / openapi / vcs / impl / LineStatusTrackerManager.kt
1 // Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
2 package com.intellij.openapi.vcs.impl
3
4 import com.google.common.collect.HashMultiset
5 import com.google.common.collect.Multiset
6 import com.intellij.diagnostic.ThreadDumper
7 import com.intellij.icons.AllIcons
8 import com.intellij.notification.Notification
9 import com.intellij.notification.NotificationAction
10 import com.intellij.notification.NotificationType
11 import com.intellij.notification.NotificationsManager
12 import com.intellij.openapi.Disposable
13 import com.intellij.openapi.application.*
14 import com.intellij.openapi.command.CommandEvent
15 import com.intellij.openapi.command.CommandListener
16 import com.intellij.openapi.command.CommandProcessor
17 import com.intellij.openapi.components.service
18 import com.intellij.openapi.diagnostic.Logger
19 import com.intellij.openapi.editor.Document
20 import com.intellij.openapi.editor.Editor
21 import com.intellij.openapi.editor.EditorFactory
22 import com.intellij.openapi.editor.event.DocumentEvent
23 import com.intellij.openapi.editor.event.DocumentListener
24 import com.intellij.openapi.editor.event.EditorFactoryEvent
25 import com.intellij.openapi.editor.event.EditorFactoryListener
26 import com.intellij.openapi.editor.ex.EditorEx
27 import com.intellij.openapi.extensions.ExtensionPointName
28 import com.intellij.openapi.fileEditor.FileDocumentManager
29 import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx
30 import com.intellij.openapi.progress.ProcessCanceledException
31 import com.intellij.openapi.progress.util.BackgroundTaskUtil
32 import com.intellij.openapi.project.Project
33 import com.intellij.openapi.util.Disposer
34 import com.intellij.openapi.util.io.FileUtilRt
35 import com.intellij.openapi.util.text.StringUtil
36 import com.intellij.openapi.vcs.*
37 import com.intellij.openapi.vcs.changes.*
38 import com.intellij.openapi.vcs.changes.conflicts.ChangelistConflictFileStatusProvider
39 import com.intellij.openapi.vcs.changes.ui.ChangesViewContentManager.Companion.LOCAL_CHANGES
40 import com.intellij.openapi.vcs.changes.ui.ChangesViewContentManager.Companion.getToolWindowFor
41 import com.intellij.openapi.vcs.checkin.CheckinHandler
42 import com.intellij.openapi.vcs.checkin.CheckinHandlerFactory
43 import com.intellij.openapi.vcs.ex.*
44 import com.intellij.openapi.vcs.history.VcsRevisionNumber
45 import com.intellij.openapi.vcs.impl.LineStatusTrackerContentLoader.ContentInfo
46 import com.intellij.openapi.vcs.impl.LineStatusTrackerContentLoader.TrackerContent
47 import com.intellij.openapi.vfs.VfsUtil
48 import com.intellij.openapi.vfs.VirtualFile
49 import com.intellij.openapi.vfs.VirtualFileManager
50 import com.intellij.openapi.vfs.newvfs.BulkFileListener
51 import com.intellij.openapi.vfs.newvfs.events.VFileDeleteEvent
52 import com.intellij.openapi.vfs.newvfs.events.VFileEvent
53 import com.intellij.openapi.vfs.newvfs.events.VFileMoveEvent
54 import com.intellij.openapi.vfs.newvfs.events.VFilePropertyChangeEvent
55 import com.intellij.testFramework.LightVirtualFile
56 import com.intellij.util.EventDispatcher
57 import com.intellij.util.concurrency.Semaphore
58 import com.intellij.util.ui.UIUtil
59 import com.intellij.vcs.commit.isNonModalCommit
60 import com.intellij.vcsUtil.VcsUtil
61 import org.jetbrains.annotations.*
62 import java.nio.charset.Charset
63 import java.util.*
64 import java.util.concurrent.Future
65 import java.util.function.Supplier
66
67 class LineStatusTrackerManager(private val project: Project) : LineStatusTrackerManagerI, Disposable {
68   private val LOCK = Any()
69   private var isDisposed = false
70
71   private val trackers = HashMap<Document, TrackerData>()
72   private val forcedDocuments = HashMap<Document, Multiset<Any>>()
73
74   private val eventDispatcher = EventDispatcher.create(Listener::class.java)
75
76   private var partialChangeListsEnabled = VcsApplicationSettings.getInstance().ENABLE_PARTIAL_CHANGELISTS
77   private val documentsInDefaultChangeList = HashSet<Document>()
78   private var clmFreezeCounter: Int = 0
79
80   private val filesWithDamagedInactiveRanges = HashSet<VirtualFile>()
81   private val fileStatesAwaitingRefresh = HashMap<VirtualFile, ChangelistsLocalLineStatusTracker.State>()
82
83   private val loader = MyBaseRevisionLoader()
84
85   companion object {
86     private val LOG = Logger.getInstance(LineStatusTrackerManager::class.java)
87
88     @JvmStatic
89     fun getInstance(project: Project): LineStatusTrackerManagerI = project.service()
90
91     @JvmStatic
92     fun getInstanceImpl(project: Project): LineStatusTrackerManager {
93       return getInstance(project) as LineStatusTrackerManager
94     }
95   }
96
97   class MyStartupActivity : VcsStartupActivity {
98     override fun runActivity(project: Project) {
99       LineStatusTrackerManager.getInstanceImpl(project).startListenForEditors()
100     }
101
102     override fun getOrder(): Int = VcsInitObject.OTHER_INITIALIZATION.order
103   }
104
105   private fun startListenForEditors() {
106     val busConnection = project.messageBus.connect()
107     busConnection.subscribe(LineStatusTrackerSettingListener.TOPIC, MyLineStatusTrackerSettingListener())
108     busConnection.subscribe(VcsFreezingProcess.Listener.TOPIC, MyFreezeListener())
109     busConnection.subscribe(CommandListener.TOPIC, MyCommandListener())
110     busConnection.subscribe(ChangeListListener.TOPIC, MyChangeListListener())
111
112     ApplicationManager.getApplication().messageBus.connect(this)
113       .subscribe(VirtualFileManager.VFS_CHANGES, MyVirtualFileListener())
114
115     LocalLineStatusTrackerProvider.EP_NAME.addChangeListener(Runnable { updateTrackingSettings() }, this)
116
117     runInEdt {
118       if (project.isDisposed) return@runInEdt
119
120       ApplicationManager.getApplication().addApplicationListener(MyApplicationListener(), this)
121       FileStatusManager.getInstance(project).addFileStatusListener(MyFileStatusListener(), this)
122
123       EditorFactory.getInstance().eventMulticaster.addDocumentListener(MyDocumentListener(), this)
124
125       MyEditorFactoryListener().install(this)
126       onEverythingChanged()
127
128       val states = project.service<PartialLineStatusTrackerManagerState>().getStatesAndClear()
129       if (states.isNotEmpty()) {
130         ChangeListManager.getInstance(project).invokeAfterUpdate({ restoreTrackersForPartiallyChangedFiles(states) },
131                                                                  InvokeAfterUpdateMode.SILENT, null, null)
132       }
133     }
134   }
135
136   override fun dispose() {
137     isDisposed = true
138     Disposer.dispose(loader)
139
140     synchronized(LOCK) {
141       for ((document, multiset) in forcedDocuments) {
142         for (requester in multiset.elementSet()) {
143           warn("Tracker is being held on dispose by $requester", document)
144         }
145       }
146       forcedDocuments.clear()
147
148       for (data in trackers.values) {
149         unregisterTrackerInCLM(data)
150         data.tracker.release()
151       }
152       trackers.clear()
153     }
154   }
155
156   override fun getLineStatusTracker(document: Document): LineStatusTracker<*>? {
157     synchronized(LOCK) {
158       return trackers[document]?.tracker
159     }
160   }
161
162   override fun getLineStatusTracker(file: VirtualFile): LineStatusTracker<*>? {
163     val document = FileDocumentManager.getInstance().getCachedDocument(file) ?: return null
164     return getLineStatusTracker(document)
165   }
166
167   @CalledInAwt
168   override fun requestTrackerFor(document: Document, requester: Any) {
169     ApplicationManager.getApplication().assertIsWriteThread()
170     synchronized(LOCK) {
171       if (isDisposed) {
172         warn("Tracker is being requested after dispose by $requester", document)
173         return
174       }
175
176       val multiset = forcedDocuments.computeIfAbsent(document) { HashMultiset.create<Any>() }
177       multiset.add(requester)
178
179       if (trackers[document] == null) {
180         val virtualFile = FileDocumentManager.getInstance().getFile(document) ?: return
181         switchTracker(virtualFile, document)
182       }
183     }
184   }
185
186   @CalledInAwt
187   override fun releaseTrackerFor(document: Document, requester: Any) {
188     ApplicationManager.getApplication().assertIsWriteThread()
189     synchronized(LOCK) {
190       val multiset = forcedDocuments[document]
191       if (multiset == null || !multiset.contains(requester)) {
192         warn("Tracker release underflow by $requester", document)
193         return
194       }
195
196       multiset.remove(requester)
197
198       if (multiset.isEmpty()) {
199         forcedDocuments.remove(document)
200         checkIfTrackerCanBeReleased(document)
201       }
202     }
203   }
204
205   override fun invokeAfterUpdate(task: Runnable) {
206     loader.addAfterUpdateRunnable(task)
207   }
208
209
210   fun getTrackers(): List<LineStatusTracker<*>> {
211     synchronized(LOCK) {
212       return trackers.values.map { it.tracker }
213     }
214   }
215
216   fun addTrackerListener(listener: Listener, disposable: Disposable) {
217     eventDispatcher.addListener(listener, disposable)
218   }
219
220   open class ListenerAdapter : Listener
221   interface Listener : EventListener {
222     fun onTrackerAdded(tracker: LineStatusTracker<*>) {
223     }
224
225     fun onTrackerRemoved(tracker: LineStatusTracker<*>) {
226     }
227   }
228
229
230   @CalledInAwt
231   private fun checkIfTrackerCanBeReleased(document: Document) {
232     synchronized(LOCK) {
233       val data = trackers[document] ?: return
234
235       if (forcedDocuments.containsKey(document)) return
236
237       if (data.tracker is ChangelistsLocalLineStatusTracker) {
238         val hasPartialChanges = data.tracker.hasPartialState()
239         if (hasPartialChanges) {
240           log("checkIfTrackerCanBeReleased - hasPartialChanges", data.tracker.virtualFile)
241           return
242         }
243
244         val isLoading = loader.hasRequestFor(document)
245         if (isLoading) {
246           log("checkIfTrackerCanBeReleased - isLoading", data.tracker.virtualFile)
247           return
248         }
249       }
250
251       releaseTracker(document)
252     }
253   }
254
255
256   @CalledInAwt
257   private fun onEverythingChanged() {
258     ApplicationManager.getApplication().assertIsWriteThread()
259     synchronized(LOCK) {
260       if (isDisposed) return
261       log("onEverythingChanged", null)
262
263       val files = HashSet<VirtualFile>()
264
265       for (data in trackers.values) {
266         files.add(data.tracker.virtualFile)
267       }
268       for (document in forcedDocuments.keys) {
269         val file = FileDocumentManager.getInstance().getFile(document)
270         if (file != null) files.add(file)
271       }
272
273       for (file in files) {
274         onFileChanged(file)
275       }
276     }
277   }
278
279   @CalledInAwt
280   private fun onFileChanged(virtualFile: VirtualFile) {
281     val document = FileDocumentManager.getInstance().getCachedDocument(virtualFile) ?: return
282
283     synchronized(LOCK) {
284       if (isDisposed) return
285       log("onFileChanged", virtualFile)
286       val tracker = trackers[document]?.tracker
287
288       if (tracker != null || forcedDocuments.containsKey(document)) {
289         switchTracker(virtualFile, document, refreshExisting = true)
290       }
291     }
292   }
293
294   private fun registerTrackerInCLM(data: TrackerData) {
295     val tracker = data.tracker
296     if (tracker !is ChangelistsLocalLineStatusTracker) return
297
298     val filePath = VcsUtil.getFilePath(tracker.virtualFile)
299     if (data.clmFilePath != null) {
300       LOG.error("[registerTrackerInCLM] tracker already registered")
301       return
302     }
303
304     ChangeListManagerImpl.getInstanceImpl(project).registerChangeTracker(filePath, tracker)
305     data.clmFilePath = filePath
306   }
307
308   private fun unregisterTrackerInCLM(data: TrackerData) {
309     val tracker = data.tracker
310     if (tracker !is ChangelistsLocalLineStatusTracker) return
311
312     val filePath = data.clmFilePath
313     if (filePath == null) {
314       LOG.error("[unregisterTrackerInCLM] tracker is not registered")
315       return
316     }
317
318     ChangeListManagerImpl.getInstanceImpl(project).unregisterChangeTracker(filePath, tracker)
319     data.clmFilePath = null
320
321     val actualFilePath = VcsUtil.getFilePath(tracker.virtualFile)
322     if (filePath != actualFilePath) {
323       LOG.error("[unregisterTrackerInCLM] unexpected file path: expected: $filePath, actual: $actualFilePath")
324     }
325   }
326
327   private fun reregisterTrackerInCLM(data: TrackerData) {
328     val tracker = data.tracker
329     if (tracker !is ChangelistsLocalLineStatusTracker) return
330
331     val oldFilePath = data.clmFilePath
332     val newFilePath = VcsUtil.getFilePath(tracker.virtualFile)
333
334     if (oldFilePath == null) {
335       LOG.error("[reregisterTrackerInCLM] tracker is not registered")
336       return
337     }
338
339     if (oldFilePath != newFilePath) {
340       ChangeListManagerImpl.getInstanceImpl(project).unregisterChangeTracker(oldFilePath, tracker)
341       ChangeListManagerImpl.getInstanceImpl(project).registerChangeTracker(newFilePath, tracker)
342       data.clmFilePath = newFilePath
343     }
344   }
345
346   private fun canCreateTrackerFor(virtualFile: VirtualFile?): Boolean {
347     if (isDisposed) return false
348     if (virtualFile == null || virtualFile is LightVirtualFile) return false
349     if (runReadAction { !virtualFile.isValid || virtualFile.fileType.isBinary || FileUtilRt.isTooLarge(virtualFile.length) }) return false
350     return true
351   }
352
353   override fun arePartialChangelistsEnabled(virtualFile: VirtualFile): Boolean {
354     if (!partialChangeListsEnabled) return false
355
356     val vcs = VcsUtil.getVcsFor(project, virtualFile)
357     return vcs != null && vcs.arePartialChangelistsSupported()
358   }
359
360
361   private fun switchTracker(virtualFile: VirtualFile, document: Document,
362                             refreshExisting: Boolean = false) {
363     val provider = getTrackerProvider(virtualFile)
364
365     val oldTracker = trackers[document]?.tracker
366     if (oldTracker != null && provider != null && provider.isMyTracker(oldTracker)) {
367       if (refreshExisting) {
368         refreshTracker(oldTracker, provider)
369       }
370     }
371     else {
372       releaseTracker(document)
373       if (provider != null) installTracker(virtualFile, document, provider)
374     }
375   }
376
377   private fun installTracker(virtualFile: VirtualFile, document: Document,
378                              provider: LocalLineStatusTrackerProvider): LocalLineStatusTracker<*>? {
379     if (isDisposed) return null
380     if (trackers[document] != null) return null
381
382     val tracker = provider.createTracker(project, virtualFile) ?: return null
383     tracker.mode = getTrackingMode()
384
385     val data = TrackerData(tracker)
386     val replacedData = trackers.put(document, data)
387     LOG.assertTrue(replacedData == null)
388
389     registerTrackerInCLM(data)
390     refreshTracker(tracker, provider)
391     eventDispatcher.multicaster.onTrackerAdded(tracker)
392
393     if (clmFreezeCounter > 0) {
394       tracker.freeze()
395     }
396
397     log("Tracker installed", virtualFile)
398     return tracker
399   }
400
401   private fun getTrackerProvider(virtualFile: VirtualFile): LocalLineStatusTrackerProvider? {
402     if (!canCreateTrackerFor(virtualFile)) return null
403
404     val customTracker = LocalLineStatusTrackerProvider.EP_NAME.findFirstSafe { it.isTrackedFile(project, virtualFile) }
405     if (customTracker != null) return customTracker
406
407     return listOf(ChangelistsLocalStatusTrackerProvider, DefaultLocalStatusTrackerProvider).find { it.isTrackedFile(project, virtualFile) }
408   }
409
410   @CalledInAwt
411   private fun releaseTracker(document: Document) {
412     val data = trackers.remove(document) ?: return
413
414     eventDispatcher.multicaster.onTrackerRemoved(data.tracker)
415     unregisterTrackerInCLM(data)
416     data.tracker.release()
417
418     log("Tracker released", data.tracker.virtualFile)
419   }
420
421   private fun updateTrackingSettings() {
422     synchronized(LOCK) {
423       if (isDisposed) return
424       val mode = getTrackingMode()
425       for (data in trackers.values) {
426         data.tracker.mode = mode
427       }
428     }
429
430     onEverythingChanged()
431   }
432
433   private fun getTrackingMode(): LocalLineStatusTracker.Mode {
434     val settings = VcsApplicationSettings.getInstance()
435     return LocalLineStatusTracker.Mode(settings.SHOW_LST_GUTTER_MARKERS,
436                                        settings.SHOW_LST_ERROR_STRIPE_MARKERS,
437                                        settings.SHOW_WHITESPACES_IN_LST)
438   }
439
440   @CalledInAwt
441   private fun refreshTracker(tracker: LocalLineStatusTracker<*>,
442                              provider: LocalLineStatusTrackerProvider) {
443     if (isDisposed) return
444     if (provider !is LineStatusTrackerContentLoader) return
445     loader.scheduleRefresh(RefreshRequest(tracker.document, provider))
446
447     log("Refresh queued", tracker.virtualFile)
448   }
449
450   private inner class MyBaseRevisionLoader : SingleThreadLoader<RefreshRequest, RefreshData>() {
451     fun hasRequestFor(document: Document): Boolean {
452       return hasRequest { it.document == document }
453     }
454
455     override fun loadRequest(request: RefreshRequest): Result<RefreshData> {
456       if (isDisposed) return Result.Canceled()
457       val document = request.document
458       val virtualFile = FileDocumentManager.getInstance().getFile(document)
459       val loader = request.loader
460
461       log("Loading started", virtualFile)
462
463       if (virtualFile == null || !virtualFile.isValid) {
464         log("Loading error: virtual file is not valid", virtualFile)
465         return Result.Error()
466       }
467
468       if (!canCreateTrackerFor(virtualFile) || !loader.isTrackedFile(project, virtualFile)) {
469         log("Loading error: virtual file is not a tracked file", virtualFile)
470         return Result.Error()
471       }
472
473       val newContentInfo = loader.getContentInfo(project, virtualFile)
474       if (newContentInfo == null) {
475         log("Loading error: base revision not found", virtualFile)
476         return Result.Error()
477       }
478
479       synchronized(LOCK) {
480         val data = trackers[document]
481         if (data == null) {
482           log("Loading cancelled: tracker not found", virtualFile)
483           return Result.Canceled()
484         }
485
486         if (!loader.shouldBeUpdated(data.contentInfo, newContentInfo)) {
487           log("Loading cancelled: no need to update", virtualFile)
488           return Result.Canceled()
489         }
490       }
491
492       val content = loader.loadContent(project, newContentInfo)
493       if (content == null) {
494         log("Loading error: provider failure", virtualFile)
495         return Result.Error()
496       }
497
498       log("Loading successful", virtualFile)
499       return Result.Success(RefreshData(content, newContentInfo))
500     }
501
502     @CalledInAwt
503     override fun handleResult(request: RefreshRequest, result: Result<RefreshData>) {
504       val document = request.document
505       when (result) {
506         is Result.Canceled -> handleCanceled(document)
507         is Result.Error -> handleError(request, document)
508         is Result.Success -> handleSuccess(request, document, result.data)
509       }
510
511       checkIfTrackerCanBeReleased(document)
512     }
513
514     private fun handleCanceled(document: Document) {
515       restorePendingTrackerState(document)
516     }
517
518     private fun handleError(request: RefreshRequest, document: Document) {
519       synchronized(LOCK) {
520         val loader = request.loader
521         val data = trackers[document] ?: return
522         val tracker = data.tracker
523
524         if (loader.isMyTracker(tracker)) {
525           loader.handleLoadingError(tracker)
526         }
527         data.contentInfo = null
528       }
529     }
530
531     private fun handleSuccess(request: RefreshRequest, document: Document, refreshData: RefreshData) {
532       val virtualFile = FileDocumentManager.getInstance().getFile(document)!!
533       val loader = request.loader
534
535       val tracker: LocalLineStatusTracker<*>
536       synchronized(LOCK) {
537         val data = trackers[document]
538         if (data == null) {
539           log("Loading finished: tracker already released", virtualFile)
540           return
541         }
542         if (!loader.shouldBeUpdated(data.contentInfo, refreshData.contentInfo)) {
543           log("Loading finished: no need to update", virtualFile)
544           return
545         }
546
547         data.contentInfo = refreshData.contentInfo
548         tracker = data.tracker
549       }
550
551       if (loader.isMyTracker(tracker)) {
552         loader.setLoadedContent(tracker, refreshData.content)
553         log("Loading finished: success", virtualFile)
554       }
555       else {
556         log("Loading finished: wrong tracker. tracker: $tracker, loader: $loader", virtualFile)
557       }
558
559       restorePendingTrackerState(document)
560     }
561
562     private fun restorePendingTrackerState(document: Document) {
563       val tracker = getLineStatusTracker(document)
564       if (tracker is ChangelistsLocalLineStatusTracker) {
565         val virtualFile = tracker.virtualFile
566
567         val state = synchronized(LOCK) {
568           fileStatesAwaitingRefresh.remove(virtualFile) ?: return
569         }
570
571         val success = tracker.restoreState(state)
572         log("Pending state restored. success - $success", virtualFile)
573       }
574     }
575   }
576
577   /**
578    * We can speedup initial content loading if it was already loaded by someone.
579    * We do not set 'contentInfo' here to ensure, that following refresh will fix potential inconsistency.
580    */
581   @CalledInAwt
582   @ApiStatus.Internal
583   fun offerTrackerContent(document: Document, text: CharSequence) {
584     val tracker: LocalLineStatusTracker<*>
585     synchronized(LOCK) {
586       val data = trackers[document]
587       if (data == null || data.contentInfo != null) return
588
589       tracker = data.tracker
590     }
591
592     if (tracker is LocalLineStatusTrackerImpl<*>) {
593       tracker.setBaseRevision(text)
594       log("Offered content", tracker.virtualFile)
595     }
596   }
597
598   private inner class MyFileStatusListener : FileStatusListener {
599     override fun fileStatusesChanged() {
600       onEverythingChanged()
601     }
602
603     override fun fileStatusChanged(virtualFile: VirtualFile) {
604       onFileChanged(virtualFile)
605     }
606   }
607
608   private inner class MyEditorFactoryListener : EditorFactoryListener {
609     fun install(disposable: Disposable) {
610       val editorFactory = EditorFactory.getInstance()
611       for (editor in editorFactory.allEditors) {
612         if (isTrackedEditor(editor)) {
613           requestTrackerFor(editor.document, editor)
614         }
615       }
616       editorFactory.addEditorFactoryListener(this, disposable)
617     }
618
619     override fun editorCreated(event: EditorFactoryEvent) {
620       val editor = event.editor
621       if (isTrackedEditor(editor)) {
622         requestTrackerFor(editor.document, editor)
623       }
624     }
625
626     override fun editorReleased(event: EditorFactoryEvent) {
627       val editor = event.editor
628       if (isTrackedEditor(editor)) {
629         releaseTrackerFor(editor.document, editor)
630       }
631     }
632
633     private fun isTrackedEditor(editor: Editor): Boolean {
634       // can't filter out "!isInLocalFileSystem" files, custom VcsBaseContentProvider can handle them
635       if (FileDocumentManager.getInstance().getFile(editor.document) == null) {
636         return false
637       }
638       return editor.project == null || editor.project == project
639     }
640   }
641
642   private inner class MyVirtualFileListener : BulkFileListener {
643     override fun before(events: List<VFileEvent>) {
644       for (event in events) {
645         when (event) {
646           is VFileDeleteEvent -> handleFileDeletion(event.file)
647         }
648       }
649     }
650
651     override fun after(events: List<VFileEvent>) {
652       for (event in events) {
653         when (event) {
654           is VFilePropertyChangeEvent -> when {
655             VirtualFile.PROP_ENCODING == event.propertyName -> onFileChanged(event.file)
656             event.isRename -> handleFileMovement(event.file)
657           }
658           is VFileMoveEvent -> handleFileMovement(event.file)
659         }
660       }
661     }
662
663     private fun handleFileMovement(file: VirtualFile) {
664       if (!partialChangeListsEnabled) return
665
666       synchronized(LOCK) {
667         forEachTrackerUnder(file) { data ->
668           reregisterTrackerInCLM(data)
669         }
670       }
671     }
672
673     private fun handleFileDeletion(file: VirtualFile) {
674       if (!partialChangeListsEnabled) return
675
676       synchronized(LOCK) {
677         forEachTrackerUnder(file) { data ->
678           releaseTracker(data.tracker.document)
679         }
680       }
681     }
682
683     private fun forEachTrackerUnder(file: VirtualFile, action: (TrackerData) -> Unit) {
684       if (file.isDirectory) {
685         val affected = trackers.values.filter { VfsUtil.isAncestor(file, it.tracker.virtualFile, false) }
686         for (data in affected) {
687           action(data)
688         }
689       }
690       else {
691         val document = FileDocumentManager.getInstance().getCachedDocument(file) ?: return
692         val data = trackers[document] ?: return
693
694         action(data)
695       }
696     }
697   }
698
699   private inner class MyDocumentListener : DocumentListener {
700     override fun documentChanged(event: DocumentEvent) {
701       if (!ApplicationManager.getApplication().isDispatchThread) return // disable for documents forUseInNonAWTThread
702       if (!partialChangeListsEnabled || project.isDisposed) return
703
704       val document = event.document
705       if (documentsInDefaultChangeList.contains(document)) return
706
707       val virtualFile = FileDocumentManager.getInstance().getFile(document) ?: return
708       if (getLineStatusTracker(document) != null) return
709
710       val provider = getTrackerProvider(virtualFile)
711       if (provider != ChangelistsLocalStatusTrackerProvider) return
712
713       val changeList = ChangeListManagerImpl.getInstanceImpl(project).getChangeList(virtualFile)
714       if (changeList != null && !changeList.isDefault) {
715         log("Tracker install from DocumentListener: ", virtualFile)
716
717         val tracker = synchronized(LOCK) {
718           installTracker(virtualFile, document, provider)
719         }
720         if (tracker is ChangelistsLocalLineStatusTracker) {
721           tracker.replayChangesFromDocumentEvents(listOf(event))
722         }
723       }
724       else {
725         documentsInDefaultChangeList.add(document)
726       }
727     }
728   }
729
730   private inner class MyApplicationListener : ApplicationListener {
731     override fun afterWriteActionFinished(action: Any) {
732       documentsInDefaultChangeList.clear()
733
734       synchronized(LOCK) {
735         val documents = trackers.values.map { it.tracker.document }
736         for (document in documents) {
737           checkIfTrackerCanBeReleased(document)
738         }
739       }
740     }
741   }
742
743   private inner class MyLineStatusTrackerSettingListener : LineStatusTrackerSettingListener {
744     override fun settingsUpdated() {
745       partialChangeListsEnabled = VcsApplicationSettings.getInstance().ENABLE_PARTIAL_CHANGELISTS
746
747       updateTrackingSettings()
748     }
749   }
750
751   private inner class MyChangeListListener : ChangeListAdapter() {
752     override fun defaultListChanged(oldDefaultList: ChangeList?, newDefaultList: ChangeList?) {
753       runInEdt(ModalityState.any()) {
754         if (project.isDisposed) return@runInEdt
755
756         expireInactiveRangesDamagedNotifications()
757
758         EditorFactory.getInstance().allEditors
759           .filterIsInstance(EditorEx::class.java)
760           .forEach {
761             it.gutterComponentEx.repaint()
762           }
763       }
764     }
765   }
766
767   private inner class MyCommandListener : CommandListener {
768     override fun commandFinished(event: CommandEvent) {
769       if (!partialChangeListsEnabled) return
770
771       if (CommandProcessor.getInstance().currentCommand == null &&
772           !filesWithDamagedInactiveRanges.isEmpty()) {
773         showInactiveRangesDamagedNotification()
774       }
775     }
776   }
777
778   class CheckinFactory : CheckinHandlerFactory() {
779     override fun createHandler(panel: CheckinProjectPanel, commitContext: CommitContext): CheckinHandler {
780       val project = panel.project
781       return object : CheckinHandler() {
782         override fun checkinSuccessful() {
783           resetExcludedFromCommit()
784         }
785
786         override fun checkinFailed(exception: MutableList<VcsException>?) {
787           resetExcludedFromCommit()
788         }
789
790         private fun resetExcludedFromCommit() {
791           runInEdt {
792             // TODO Move this to SingleChangeListCommitWorkflow
793             if (!project.isDisposed && !panel.isNonModalCommit) getInstanceImpl(project).resetExcludedFromCommitMarkers()
794           }
795         }
796       }
797     }
798   }
799
800   private inner class MyFreezeListener : VcsFreezingProcess.Listener {
801     override fun onFreeze() {
802       runReadAction {
803         synchronized(LOCK) {
804           if (clmFreezeCounter == 0) {
805             for (data in trackers.values) {
806               try {
807                 data.tracker.freeze()
808               }
809               catch (e: Throwable) {
810                 LOG.error(e)
811               }
812             }
813           }
814           clmFreezeCounter++
815         }
816       }
817     }
818
819     override fun onUnfreeze() {
820       runInEdt(ModalityState.any()) {
821         synchronized(LOCK) {
822           clmFreezeCounter--
823           if (clmFreezeCounter == 0) {
824             for (data in trackers.values) {
825               try {
826                 data.tracker.unfreeze()
827               }
828               catch (e: Throwable) {
829                 LOG.error(e)
830               }
831             }
832           }
833         }
834       }
835     }
836   }
837
838
839   private class TrackerData(val tracker: LocalLineStatusTracker<*>,
840                             var contentInfo: ContentInfo? = null,
841                             var clmFilePath: FilePath? = null)
842
843   private class RefreshRequest(val document: Document, val loader: LineStatusTrackerContentLoader) {
844     override fun equals(other: Any?): Boolean = other is RefreshRequest && document == other.document
845     override fun hashCode(): Int = document.hashCode()
846     override fun toString(): String = "RefreshRequest: " + (FileDocumentManager.getInstance().getFile(document)?.path ?: "unknown")
847   }
848
849   private class RefreshData(val content: TrackerContent,
850                             val contentInfo: ContentInfo)
851
852
853   private fun log(message: String, file: VirtualFile?) {
854     if (LOG.isDebugEnabled) {
855       if (file != null) {
856         LOG.debug(message + "; file: " + file.path)
857       }
858       else {
859         LOG.debug(message)
860       }
861     }
862   }
863
864   private fun warn(message: String, document: Document?) {
865     val file = document?.let { FileDocumentManager.getInstance().getFile(it) }
866     warn(message, file)
867   }
868
869   private fun warn(message: String, file: VirtualFile?) {
870     if (file != null) {
871       LOG.warn(message + "; file: " + file.path)
872     }
873     else {
874       LOG.warn(message)
875     }
876   }
877
878
879   @CalledInAwt
880   fun resetExcludedFromCommitMarkers() {
881     ApplicationManager.getApplication().assertIsWriteThread()
882     synchronized(LOCK) {
883       val documents = mutableListOf<Document>()
884
885       for (data in trackers.values) {
886         val tracker = data.tracker
887         if (tracker is ChangelistsLocalLineStatusTracker) {
888           tracker.resetExcludedFromCommitMarkers()
889           documents.add(tracker.document)
890         }
891       }
892
893       for (document in documents) {
894         checkIfTrackerCanBeReleased(document)
895       }
896     }
897   }
898
899
900   @CalledInAwt
901   internal fun collectPartiallyChangedFilesStates(): List<ChangelistsLocalLineStatusTracker.FullState> {
902     ApplicationManager.getApplication().assertIsWriteThread()
903     val result = mutableListOf<ChangelistsLocalLineStatusTracker.FullState>()
904     synchronized(LOCK) {
905       for (data in trackers.values) {
906         val tracker = data.tracker
907         if (tracker is ChangelistsLocalLineStatusTracker) {
908           val hasPartialChanges = tracker.affectedChangeListsIds.size > 1
909           if (hasPartialChanges) {
910             result.add(tracker.storeTrackerState())
911           }
912         }
913       }
914     }
915     return result
916   }
917
918   @CalledInAwt
919   private fun restoreTrackersForPartiallyChangedFiles(trackerStates: List<ChangelistsLocalLineStatusTracker.State>) {
920     runWriteAction {
921       synchronized(LOCK) {
922         if (isDisposed) return@runWriteAction
923         for (state in trackerStates) {
924           val virtualFile = state.virtualFile
925           val document = FileDocumentManager.getInstance().getDocument(virtualFile) ?: continue
926
927           val provider = getTrackerProvider(virtualFile)
928           if (provider != ChangelistsLocalStatusTrackerProvider) continue
929
930           switchTracker(virtualFile, document)
931
932           val tracker = trackers[document]?.tracker
933           if (tracker !is ChangelistsLocalLineStatusTracker) continue
934
935           val isLoading = loader.hasRequestFor(document)
936           if (isLoading) {
937             fileStatesAwaitingRefresh.put(state.virtualFile, state)
938             log("State restoration scheduled", virtualFile)
939           }
940           else {
941             val success = tracker.restoreState(state)
942             log("State restored. success - $success", virtualFile)
943           }
944         }
945
946         loader.addAfterUpdateRunnable(Runnable {
947           synchronized(LOCK) {
948             log("State restoration finished", null)
949             fileStatesAwaitingRefresh.clear()
950           }
951         })
952       }
953     }
954   }
955
956
957   @CalledInAwt
958   internal fun notifyInactiveRangesDamaged(virtualFile: VirtualFile) {
959     ApplicationManager.getApplication().assertIsWriteThread()
960     if (filesWithDamagedInactiveRanges.contains(virtualFile) || virtualFile == FileEditorManagerEx.getInstanceEx(project).currentFile) {
961       return
962     }
963     filesWithDamagedInactiveRanges.add(virtualFile)
964   }
965
966   private fun showInactiveRangesDamagedNotification() {
967     val currentNotifications = NotificationsManager.getNotificationsManager()
968       .getNotificationsOfType(InactiveRangesDamagedNotification::class.java, project)
969
970     val lastNotification = currentNotifications.lastOrNull { !it.isExpired }
971     if (lastNotification != null) filesWithDamagedInactiveRanges.addAll(lastNotification.virtualFiles)
972
973     currentNotifications.forEach { it.expire() }
974
975     val files = filesWithDamagedInactiveRanges.toSet()
976     filesWithDamagedInactiveRanges.clear()
977
978     InactiveRangesDamagedNotification(project, files).notify(project)
979   }
980
981   @CalledInAwt
982   private fun expireInactiveRangesDamagedNotifications() {
983     filesWithDamagedInactiveRanges.clear()
984
985     val currentNotifications = NotificationsManager.getNotificationsManager()
986       .getNotificationsOfType(InactiveRangesDamagedNotification::class.java, project)
987     currentNotifications.forEach { it.expire() }
988   }
989
990   private class InactiveRangesDamagedNotification(project: Project, val virtualFiles: Set<VirtualFile>)
991     : Notification(VcsNotifier.STANDARD_NOTIFICATION.displayId,
992                    AllIcons.Toolwindows.ToolWindowChanges,
993                    null,
994                    null,
995                    VcsBundle.getString("lst.inactive.ranges.damaged.notification"),
996                    NotificationType.INFORMATION,
997                    null) {
998     init {
999       addAction(NotificationAction.createSimple(
1000         Supplier { VcsBundle.message("action.NotificationAction.InactiveRangesDamagedNotification.text.view.changes") },
1001         Runnable {
1002           val defaultList = ChangeListManager.getInstance(project).defaultChangeList
1003           val changes = defaultList.changes.filter { virtualFiles.contains(it.virtualFile) }
1004
1005           val window = getToolWindowFor(project, LOCAL_CHANGES)
1006           window?.activate { ChangesViewManager.getInstance(project).selectChanges(changes) }
1007           expire()
1008         }))
1009     }
1010   }
1011
1012
1013   @TestOnly
1014   fun waitUntilBaseContentsLoaded() {
1015     assert(ApplicationManager.getApplication().isUnitTestMode)
1016
1017     if (ApplicationManager.getApplication().isDispatchThread) {
1018       UIUtil.dispatchAllInvocationEvents()
1019     }
1020
1021     val semaphore = Semaphore()
1022     semaphore.down()
1023
1024     loader.addAfterUpdateRunnable(Runnable {
1025       semaphore.up()
1026     })
1027
1028     val start = System.currentTimeMillis()
1029     while (true) {
1030       if (ApplicationManager.getApplication().isDispatchThread) {
1031         UIUtil.dispatchAllInvocationEvents()
1032       }
1033       if (semaphore.waitFor(10)) {
1034         return
1035       }
1036       if (System.currentTimeMillis() - start > 10000) {
1037         loader.dumpInternalState()
1038         System.err.println(ThreadDumper.dumpThreadsToString())
1039         throw IllegalStateException("Couldn't await base contents")
1040       }
1041     }
1042   }
1043
1044   @TestOnly
1045   fun releaseAllTrackers() {
1046     synchronized(LOCK) {
1047       forcedDocuments.clear()
1048
1049       for (data in trackers.values) {
1050         unregisterTrackerInCLM(data)
1051         data.tracker.release()
1052       }
1053       trackers.clear()
1054     }
1055   }
1056 }
1057
1058
1059 /**
1060  * Single threaded queue with the following properties:
1061  * - Ignores duplicated requests (the first queued is used).
1062  * - Allows to check whether request is scheduled or is waiting for completion.
1063  * - Notifies callbacks when queue is exhausted.
1064  */
1065 private abstract class SingleThreadLoader<Request, T> : Disposable {
1066   private val LOG = Logger.getInstance(SingleThreadLoader::class.java)
1067   private val LOCK: Any = Any()
1068
1069   private val taskQueue = ArrayDeque<Request>()
1070   private val waitingForRefresh = HashSet<Request>()
1071
1072   private val callbacksWaitingUpdateCompletion = ArrayList<Runnable>()
1073
1074   private var isScheduled: Boolean = false
1075   private var isDisposed: Boolean = false
1076   private var lastFuture: Future<*>? = null
1077
1078   @CalledInBackground
1079   protected abstract fun loadRequest(request: Request): Result<T>
1080
1081   @CalledInAwt
1082   protected abstract fun handleResult(request: Request, result: Result<T>)
1083
1084
1085   @CalledInAwt
1086   fun scheduleRefresh(request: Request) {
1087     if (isDisposed) return
1088
1089     synchronized(LOCK) {
1090       if (taskQueue.contains(request)) return
1091       taskQueue.add(request)
1092
1093       schedule()
1094     }
1095   }
1096
1097   @CalledInAwt
1098   override fun dispose() {
1099     val callbacks = mutableListOf<Runnable>()
1100     synchronized(LOCK) {
1101       isDisposed = true
1102       taskQueue.clear()
1103       waitingForRefresh.clear()
1104       lastFuture?.cancel(true)
1105
1106       callbacks += callbacksWaitingUpdateCompletion
1107       callbacksWaitingUpdateCompletion.clear()
1108     }
1109
1110     executeCallbacks(callbacksWaitingUpdateCompletion)
1111   }
1112
1113   @CalledInAwt
1114   protected fun hasRequest(condition: (Request) -> Boolean): Boolean {
1115     synchronized(LOCK) {
1116       return taskQueue.any(condition) ||
1117              waitingForRefresh.any(condition)
1118     }
1119   }
1120
1121   @CalledInAny
1122   fun addAfterUpdateRunnable(task: Runnable) {
1123     val updateScheduled = putRunnableIfUpdateScheduled(task)
1124     if (updateScheduled) return
1125
1126     runInEdt(ModalityState.any()) {
1127       if (!putRunnableIfUpdateScheduled(task)) {
1128         task.run()
1129       }
1130     }
1131   }
1132
1133   private fun putRunnableIfUpdateScheduled(task: Runnable): Boolean {
1134     synchronized(LOCK) {
1135       if (taskQueue.isEmpty() && waitingForRefresh.isEmpty()) return false
1136       callbacksWaitingUpdateCompletion.add(task)
1137       return true
1138     }
1139   }
1140
1141
1142   private fun schedule() {
1143     if (isDisposed) return
1144
1145     synchronized(LOCK) {
1146       if (isScheduled) return
1147       if (taskQueue.isEmpty()) return
1148
1149       isScheduled = true
1150       lastFuture = ApplicationManager.getApplication().executeOnPooledThread {
1151         BackgroundTaskUtil.runUnderDisposeAwareIndicator(this, Runnable {
1152           handleRequests()
1153         })
1154       }
1155     }
1156   }
1157
1158   private fun handleRequests() {
1159     while (true) {
1160       val request = synchronized(LOCK) {
1161         val request = taskQueue.poll()
1162
1163         if (isDisposed || request == null) {
1164           isScheduled = false
1165           return
1166         }
1167
1168         waitingForRefresh.add(request)
1169         return@synchronized request
1170       }
1171
1172       handleSingleRequest(request)
1173     }
1174   }
1175
1176   private fun handleSingleRequest(request: Request) {
1177     val result: Result<T> = try {
1178       loadRequest(request)
1179     }
1180     catch (e: ProcessCanceledException) {
1181       Result.Canceled()
1182     }
1183     catch (e: Throwable) {
1184       LOG.error(e)
1185       Result.Error()
1186     }
1187
1188     runInEdt(ModalityState.any()) {
1189       try {
1190         synchronized(LOCK) {
1191           waitingForRefresh.remove(request)
1192         }
1193
1194         handleResult(request, result)
1195       }
1196       finally {
1197         notifyTrackerRefreshed()
1198       }
1199     }
1200   }
1201
1202   @CalledInAwt
1203   private fun notifyTrackerRefreshed() {
1204     if (isDisposed) return
1205
1206     val callbacks = mutableListOf<Runnable>()
1207     synchronized(LOCK) {
1208       if (taskQueue.isEmpty() && waitingForRefresh.isEmpty()) {
1209         callbacks += callbacksWaitingUpdateCompletion
1210         callbacksWaitingUpdateCompletion.clear()
1211       }
1212     }
1213
1214     executeCallbacks(callbacks)
1215   }
1216
1217   @CalledInAwt
1218   private fun executeCallbacks(callbacks: List<Runnable>) {
1219     for (callback in callbacks) {
1220       try {
1221         callback.run()
1222       }
1223       catch (e: ProcessCanceledException) {
1224       }
1225       catch (e: Throwable) {
1226         LOG.error(e)
1227       }
1228     }
1229   }
1230
1231   @TestOnly
1232   fun dumpInternalState() {
1233     synchronized(LOCK) {
1234       LOG.debug("isScheduled - $isScheduled")
1235       LOG.debug("pending callbacks: ${callbacksWaitingUpdateCompletion.size}")
1236
1237       taskQueue.forEach {
1238         LOG.debug("pending task: ${it}")
1239       }
1240       waitingForRefresh.forEach {
1241         LOG.debug("waiting refresh: ${it}")
1242       }
1243     }
1244   }
1245 }
1246
1247 private sealed class Result<T> {
1248   class Success<T>(val data: T) : Result<T>()
1249   class Canceled<T> : Result<T>()
1250   class Error<T> : Result<T>()
1251 }
1252
1253 private object ChangelistsLocalStatusTrackerProvider : BaseRevisionStatusTrackerContentLoader() {
1254   override fun isTrackedFile(project: Project, file: VirtualFile): Boolean {
1255     if (!LineStatusTrackerManager.getInstance(project).arePartialChangelistsEnabled(file)) return false
1256     if (!super.isTrackedFile(project, file)) return false
1257
1258     val status = FileStatusManager.getInstance(project).getStatus(file)
1259     if (status != FileStatus.MODIFIED &&
1260         status != ChangelistConflictFileStatusProvider.MODIFIED_OUTSIDE &&
1261         status != FileStatus.NOT_CHANGED) return false
1262
1263     val change = ChangeListManager.getInstance(project).getChange(file)
1264     return change != null && change.javaClass == Change::class.java &&
1265            (change.type == Change.Type.MODIFICATION || change.type == Change.Type.MOVED) &&
1266            change.afterRevision is CurrentContentRevision
1267   }
1268
1269   override fun isMyTracker(tracker: LocalLineStatusTracker<*>): Boolean = tracker is ChangelistsLocalLineStatusTracker
1270
1271   override fun createTracker(project: Project, file: VirtualFile): LocalLineStatusTracker<*>? {
1272     val document = FileDocumentManager.getInstance().getDocument(file) ?: return null
1273     return ChangelistsLocalLineStatusTracker.createTracker(project, document, file)
1274   }
1275 }
1276
1277 private object DefaultLocalStatusTrackerProvider : BaseRevisionStatusTrackerContentLoader() {
1278   override fun isMyTracker(tracker: LocalLineStatusTracker<*>): Boolean = tracker is SimpleLocalLineStatusTracker
1279
1280   override fun createTracker(project: Project, file: VirtualFile): LocalLineStatusTracker<*>? {
1281     val document = FileDocumentManager.getInstance().getDocument(file) ?: return null
1282     return SimpleLocalLineStatusTracker.createTracker(project, document, file)
1283   }
1284 }
1285
1286 private abstract class BaseRevisionStatusTrackerContentLoader : LineStatusTrackerContentLoader {
1287   override fun isTrackedFile(project: Project, file: VirtualFile): Boolean {
1288     if (!VcsFileStatusProvider.getInstance(project).isSupported(file)) return false
1289
1290     val status = FileStatusManager.getInstance(project).getStatus(file)
1291     if (status == FileStatus.ADDED ||
1292         status == FileStatus.DELETED ||
1293         status == FileStatus.UNKNOWN ||
1294         status == FileStatus.IGNORED) {
1295       return false
1296     }
1297     return true
1298   }
1299
1300   override fun getContentInfo(project: Project, file: VirtualFile): ContentInfo? {
1301     val baseContent = VcsFileStatusProvider.getInstance(project).getBaseRevision(file) ?: return null
1302     return BaseRevisionContentInfo(baseContent, file.charset)
1303   }
1304
1305   override fun shouldBeUpdated(oldInfo: ContentInfo?, newInfo: ContentInfo): Boolean {
1306     newInfo as BaseRevisionContentInfo
1307     return oldInfo == null ||
1308            oldInfo !is BaseRevisionContentInfo ||
1309            oldInfo.baseContent.revisionNumber != newInfo.baseContent.revisionNumber ||
1310            oldInfo.baseContent.revisionNumber == VcsRevisionNumber.NULL ||
1311            oldInfo.charset != newInfo.charset
1312   }
1313
1314   override fun loadContent(project: Project, info: ContentInfo): BaseRevisionContent? {
1315     info as BaseRevisionContentInfo
1316     val lastUpToDateContent = info.baseContent.loadContent() ?: return null
1317     val correctedText = StringUtil.convertLineSeparators(lastUpToDateContent)
1318     return BaseRevisionContent(correctedText)
1319   }
1320
1321   override fun setLoadedContent(tracker: LocalLineStatusTracker<*>, content: TrackerContent) {
1322     tracker as LocalLineStatusTrackerImpl<*>
1323     content as BaseRevisionContent
1324     tracker.setBaseRevision(content.text)
1325   }
1326
1327   override fun handleLoadingError(tracker: LocalLineStatusTracker<*>) {
1328     tracker as LocalLineStatusTrackerImpl<*>
1329     tracker.dropBaseRevision()
1330   }
1331
1332   private class BaseRevisionContentInfo(val baseContent: VcsBaseContentProvider.BaseContent, val charset: Charset) : ContentInfo
1333   private class BaseRevisionContent(val text: CharSequence) : TrackerContent
1334 }
1335
1336
1337 interface LocalLineStatusTrackerProvider {
1338   fun isTrackedFile(project: Project, file: VirtualFile): Boolean
1339   fun isMyTracker(tracker: LocalLineStatusTracker<*>): Boolean
1340   fun createTracker(project: Project, file: VirtualFile): LocalLineStatusTracker<*>?
1341
1342   companion object {
1343     val EP_NAME: ExtensionPointName<LocalLineStatusTrackerProvider> =
1344       ExtensionPointName.create("com.intellij.openapi.vcs.impl.LocalLineStatusTrackerProvider")
1345   }
1346 }
1347
1348 interface LineStatusTrackerContentLoader : LocalLineStatusTrackerProvider {
1349   fun getContentInfo(project: Project, file: VirtualFile): ContentInfo?
1350   fun shouldBeUpdated(oldInfo: ContentInfo?, newInfo: ContentInfo): Boolean
1351   fun loadContent(project: Project, info: ContentInfo): TrackerContent?
1352
1353   fun setLoadedContent(tracker: LocalLineStatusTracker<*>, content: TrackerContent)
1354   fun handleLoadingError(tracker: LocalLineStatusTracker<*>)
1355
1356   interface ContentInfo
1357   interface TrackerContent
1358 }