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
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
16 import kotlin.math.max
17 import kotlin.math.min
19 open class CommonInjectedFileChangesHandler(
20 shreds: List<PsiLanguageInjectionHost.Shred>,
22 fragmentDocument: Document,
24 ) : BaseInjectedFileChangesHandler(hostEditor, fragmentDocument, injectedFile) {
26 protected val markers: MutableList<MarkersMapping> =
27 LinkedList<MarkersMapping>().apply {
28 addAll(getMarkersFromShreds(shreds))
32 protected fun getMarkersFromShreds(shreds: List<PsiLanguageInjectionHost.Shred>): List<MarkersMapping> {
33 val result = ArrayList<MarkersMapping>(shreds.size)
35 val smartPointerManager = SmartPointerManager.getInstance(myProject)
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))
45 origMarker.isGreedyToRight = true
46 rangeMarker.isGreedyToRight = true
47 if (origMarker.startOffset > curOffset) {
48 origMarker.isGreedyToLeft = true
49 rangeMarker.isGreedyToLeft = true
51 curOffset = origMarker.endOffset
57 override fun isValid(): Boolean = myInjectedFile.isValid && markers.all { it.isValid() }
59 override fun commitToOriginal(e: DocumentEvent) {
60 val text = myFragmentDocument.text
61 val map = markers.groupByTo(LinkedHashMap()) { it.host }
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)
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)
90 protected fun updateInjectionHostElement(host: PsiLanguageInjectionHost,
91 insideHost: ProperTextRange,
92 content: String): PsiLanguageInjectionHost? {
93 return ElementManipulators.handleContentChange(host, insideHost, content)
96 override fun dispose() {
97 markers.forEach(MarkersMapping::dispose)
102 override fun handlesRange(range: TextRange): Boolean {
103 if (markers.isEmpty()) return false
105 val hostRange = TextRange.create(markers[0].hostMarker.startOffset,
106 markers[markers.size - 1].hostMarker.endOffset)
107 return range.intersects(hostRange)
110 protected fun fragmentMarkerFromShred(shred: PsiLanguageInjectionHost.Shred): RangeMarker =
111 myFragmentDocument.createRangeMarker(shred.innerRange)
113 protected fun failAndReport(message: String, e: DocumentEvent? = null, exception: Exception? = null): Nothing =
114 throw getReportException(message, e, exception)
116 protected fun getReportException(message: String,
118 exception: Exception?): RuntimeExceptionWithAttachments =
119 RuntimeExceptionWithAttachments("${this.javaClass.simpleName}: $message (event = $e)," +
120 " myInjectedFile.isValid = ${myInjectedFile.isValid}, isValid = $isValid",
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) }
129 protected fun MutableList<MarkersMapping>.logMarkersRanges(): String = joinToString("\n") { mm ->
130 "fragment:${myFragmentDocument.logMarker(mm.fragmentRange)} host:${logHostMarker(mm.hostMarker.range)}"
133 protected fun logHostMarker(rangeInHost: TextRange?) = myHostDocument.logMarker(rangeInHost)
135 protected fun Document.logMarker(rangeInHost: TextRange?): String = "$rangeInHost -> '${rangeInHost?.let {
139 catch (e: IndexOutOfBoundsException) {
144 protected fun String.substringVerbose(start: Int, cursor: Int): String = try {
145 substring(start, cursor)
147 catch (e: StringIndexOutOfBoundsException) {
148 failAndReport("can't get substring ($start, $cursor) of '${this}'[$length]", exception = e)
151 fun distributeTextToMarkers(affectedMarkers: List<MarkersMapping>,
152 affectedRange: TextRange,
153 limit: Int): List<Pair<MarkersMapping, String>> {
155 return affectedMarkers.indices.map { i ->
156 val marker = affectedMarkers[i]
157 val fragmentMarker = marker.fragmentMarker
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
165 min(text.length, max(fragmentMarker.endOffset, limit))
167 text.substringVerbose(start, cursor)
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
184 fragmentMarker.dispose()
189 inline val Segment.range: TextRange get() = TextRange.create(this)
191 inline val PsiLanguageInjectionHost.Shred.innerRange: TextRange
192 get() = TextRange.create(this.range.startOffset + this.prefix.length,
193 this.range.endOffset - this.suffix.length)
195 val PsiLanguageInjectionHost.contentRange
196 get() = ElementManipulators.getValueTextRange(this).shiftRight(textRange.startOffset)
198 private val PsiElement.withNextSiblings: Sequence<PsiElement>
199 get() = generateSequence(this) { it.nextSibling }
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()
208 private fun affectedLength(markersMapping: MarkersMapping?, affectedRange: TextRange): Int =
209 markersMapping?.fragmentRange?.let { affectedRange.intersection(it)?.length } ?: -1