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