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