IDEA-248039 Re-worked commit of injected fragment to not mix encoded and decoded...
[idea/community.git] / platform / lang-impl / src / com / intellij / psi / impl / source / tree / injected / changesHandler / CommonInjectedFileChangesHandler.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.psi.impl.source.tree.injected.changesHandler
3
4 import com.intellij.openapi.diagnostic.Attachment
5 import com.intellij.openapi.diagnostic.RuntimeExceptionWithAttachments
6 import com.intellij.openapi.editor.Document
7 import com.intellij.openapi.editor.Editor
8 import com.intellij.openapi.editor.RangeMarker
9 import com.intellij.openapi.editor.event.DocumentEvent
10 import com.intellij.openapi.util.ProperTextRange
11 import com.intellij.openapi.util.Segment
12 import com.intellij.openapi.util.TextRange
13 import com.intellij.psi.*
14 import org.jetbrains.annotations.ApiStatus
15 import java.util.*
16 import kotlin.math.max
17 import kotlin.math.min
18
19 open class CommonInjectedFileChangesHandler(
20   shreds: List<PsiLanguageInjectionHost.Shred>,
21   hostEditor: Editor,
22   fragmentDocument: Document,
23   injectedFile: PsiFile
24 ) : BaseInjectedFileChangesHandler(hostEditor, fragmentDocument, injectedFile) {
25
26   protected val markers: MutableList<MarkersMapping> =
27     LinkedList<MarkersMapping>().apply {
28       addAll(getMarkersFromShreds(shreds))
29     }
30
31
32   protected fun getMarkersFromShreds(shreds: List<PsiLanguageInjectionHost.Shred>): List<MarkersMapping> {
33     val result = ArrayList<MarkersMapping>(shreds.size)
34
35     val smartPointerManager = SmartPointerManager.getInstance(myProject)
36     var curOffset = -1
37     for (shred in shreds) {
38       val rangeMarker = fragmentMarkerFromShred(shred)
39       val rangeInsideHost = shred.rangeInsideHost
40       val host = shred.host ?: failAndReport("host should not be null", null, null)
41       val origMarker = myHostDocument.createRangeMarker(rangeInsideHost.shiftRight(host.textRange.startOffset))
42       val elementPointer = smartPointerManager.createSmartPsiElementPointer(host)
43       result.add(MarkersMapping(origMarker, rangeMarker, elementPointer))
44
45       origMarker.isGreedyToRight = true
46       rangeMarker.isGreedyToRight = true
47       if (origMarker.startOffset > curOffset) {
48         origMarker.isGreedyToLeft = true
49         rangeMarker.isGreedyToLeft = true
50       }
51       curOffset = origMarker.endOffset
52     }
53     return result
54   }
55
56
57   override fun isValid(): Boolean = myInjectedFile.isValid && markers.all { it.isValid() }
58
59   override fun commitToOriginal(e: DocumentEvent) {
60     val text = myFragmentDocument.text
61     val map = markers.groupByTo(LinkedHashMap()) { it.host }
62
63     val documentManager = PsiDocumentManager.getInstance(myProject)
64     documentManager.commitDocument(myHostDocument) // commit here and after each manipulator update
65     for (host in map.keys) {
66       if (host == null) continue
67       val hostRange = host.textRange
68       val hostOffset = hostRange.startOffset
69       val originalText = host.text
70       var currentHost = host;
71       for ((hostMarker, fragmentMarker, _) in map[host].orEmpty().reversed()) {
72         val localInsideHost = ProperTextRange(hostMarker.startOffset - hostOffset, hostMarker.endOffset - hostOffset)
73         val localInsideFile = ProperTextRange(fragmentMarker.startOffset, fragmentMarker.endOffset)
74
75         // fixme we could optimize here and check if host text has been changed and update only really changed fragments, not all of them
76         if (currentHost != null && localInsideFile.endOffset <= text.length && !localInsideFile.isEmpty) {
77           val decodedText = localInsideFile.substring(text)
78           currentHost = updateInjectionHostElement(currentHost, localInsideHost, decodedText)
79           if (currentHost == null) {
80             failAndReport("Updating host returned null. Original host" + host +
81                           "; original text: " + originalText +
82                           "; updated range in host: " + localInsideHost +
83                           "; decoded text to replace: " + decodedText, e)
84           }
85         }
86       }
87     }
88   }
89
90   protected fun updateInjectionHostElement(host: PsiLanguageInjectionHost,
91                                            insideHost: ProperTextRange,
92                                            content: String): PsiLanguageInjectionHost? {
93     return ElementManipulators.handleContentChange(host, insideHost, content)
94   }
95
96   override fun dispose() {
97     markers.forEach(MarkersMapping::dispose)
98     markers.clear()
99     super.dispose()
100   }
101
102   override fun handlesRange(range: TextRange): Boolean {
103     if (markers.isEmpty()) return false
104
105     val hostRange = TextRange.create(markers[0].hostMarker.startOffset,
106                                      markers[markers.size - 1].hostMarker.endOffset)
107     return range.intersects(hostRange)
108   }
109
110   protected fun fragmentMarkerFromShred(shred: PsiLanguageInjectionHost.Shred): RangeMarker =
111     myFragmentDocument.createRangeMarker(shred.innerRange)
112
113   protected fun failAndReport(message: String, e: DocumentEvent? = null, exception: Exception? = null): Nothing =
114     throw getReportException(message, e, exception)
115
116   protected fun getReportException(message: String,
117                                    e: DocumentEvent?,
118                                    exception: Exception?): RuntimeExceptionWithAttachments =
119     RuntimeExceptionWithAttachments("${this.javaClass.simpleName}: $message (event = $e)," +
120                                     " myInjectedFile.isValid = ${myInjectedFile.isValid}, isValid = $isValid",
121                                     *listOfNotNull(
122                                       Attachment("hosts", markers.joinToString("\n\n") { it.host?.text ?: "<null>" }),
123                                       Attachment("markers", markers.logMarkersRanges()),
124                                       Attachment("injected document", this.myFragmentDocument.text),
125                                       exception?.let { Attachment("exception", it) }
126                                     ).toTypedArray()
127     )
128
129   protected fun MutableList<MarkersMapping>.logMarkersRanges(): String = joinToString("\n") { mm ->
130     "fragment:${myFragmentDocument.logMarker(mm.fragmentRange)} host:${logHostMarker(mm.hostMarker.range)}"
131   }
132
133   protected fun logHostMarker(rangeInHost: TextRange?) = myHostDocument.logMarker(rangeInHost)
134
135   protected fun Document.logMarker(rangeInHost: TextRange?): String = "$rangeInHost -> '${rangeInHost?.let {
136     try {
137       getText(it)
138     }
139     catch (e: IndexOutOfBoundsException) {
140       e.toString()
141     }
142   }}'"
143
144   protected fun String.substringVerbose(start: Int, cursor: Int): String = try {
145     substring(start, cursor)
146   }
147   catch (e: StringIndexOutOfBoundsException) {
148     failAndReport("can't get substring ($start, $cursor) of '${this}'[$length]", exception = e)
149   }
150
151   fun distributeTextToMarkers(affectedMarkers: List<MarkersMapping>,
152                               affectedRange: TextRange,
153                               limit: Int): List<Pair<MarkersMapping, String>> {
154     var cursor = 0
155     return affectedMarkers.indices.map { i ->
156       val marker = affectedMarkers[i]
157       val fragmentMarker = marker.fragmentMarker
158
159       marker to if (fragmentMarker.isValid) {
160         val start = max(cursor, fragmentMarker.startOffset)
161         val text = fragmentMarker.document.text
162         cursor = if (affectedLength(marker, affectedRange) == 0 && affectedLength(affectedMarkers.getOrNull(i + 1), affectedRange) > 1)
163           affectedMarkers.getOrNull(i + 1)!!.fragmentMarker.startOffset
164         else
165           min(text.length, max(fragmentMarker.endOffset, limit))
166
167         text.substringVerbose(start, cursor)
168       }
169       else ""
170     }
171   }
172
173 }
174
175
176 data class MarkersMapping(val hostMarker: RangeMarker,
177                           val fragmentMarker: RangeMarker,
178                           val hostPointer: SmartPsiElementPointer<PsiLanguageInjectionHost>) {
179   val host: PsiLanguageInjectionHost? get() = hostPointer.element
180   val hostElementRange: TextRange? get() = hostPointer.range?.range
181   val fragmentRange: TextRange get() = fragmentMarker.range
182   fun isValid(): Boolean = hostMarker.isValid && fragmentMarker.isValid && hostPointer.element?.isValid == true
183   fun dispose() {
184     fragmentMarker.dispose()
185     hostMarker.dispose()
186   }
187 }
188
189 inline val Segment.range: TextRange get() = TextRange.create(this)
190
191 inline val PsiLanguageInjectionHost.Shred.innerRange: TextRange
192   get() = TextRange.create(this.range.startOffset + this.prefix.length,
193                            this.range.endOffset - this.suffix.length)
194
195 val PsiLanguageInjectionHost.contentRange
196   get() = ElementManipulators.getValueTextRange(this).shiftRight(textRange.startOffset)
197
198 private val PsiElement.withNextSiblings: Sequence<PsiElement>
199   get() = generateSequence(this) { it.nextSibling }
200
201 @ApiStatus.Internal
202 fun getInjectionHostAtRange(hostPsiFile: PsiFile, contextRange: Segment): PsiLanguageInjectionHost? =
203   hostPsiFile.findElementAt(contextRange.startOffset)?.withNextSiblings.orEmpty()
204     .takeWhile { it.textRange.startOffset < contextRange.endOffset }
205     .flatMap { sequenceOf(it, it.parent) }
206     .filterIsInstance<PsiLanguageInjectionHost>().firstOrNull()
207
208 private fun affectedLength(markersMapping: MarkersMapping?, affectedRange: TextRange): Int =
209   markersMapping?.fragmentRange?.let { affectedRange.intersection(it)?.length } ?: -1