diff --git a/platform/lang-api/src/com/intellij/psi/util/PartiallyKnownString.kt b/platform/lang-api/src/com/intellij/psi/util/PartiallyKnownString.kt new file mode 100644 index 000000000000..f59f79932346 --- /dev/null +++ b/platform/lang-api/src/com/intellij/psi/util/PartiallyKnownString.kt @@ -0,0 +1,179 @@ +// 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. +package com.intellij.psi.util + +import com.intellij.openapi.util.TextRange +import com.intellij.psi.ElementManipulators +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiLanguageInjectionHost +import com.intellij.util.SmartList +import com.intellij.util.containers.toHeadAndTail +import org.jetbrains.annotations.ApiStatus + +@ApiStatus.Experimental +sealed class StringEntry { + abstract val sourcePsi: PsiElement? // maybe it should be PsiLanguageInjectionHost and only for `Known` values + abstract val range: TextRange + + class Known(val value: String, override val sourcePsi: PsiElement?, override val range: TextRange) : StringEntry() + class Unknown(override val sourcePsi: PsiElement?, override val range: TextRange) : StringEntry() + + val rangeAlignedToHost: TextRange? + get() { + val entry = this + val sourcePsi = entry.sourcePsi ?: return null + if (sourcePsi is PsiLanguageInjectionHost) return entry.range + if (sourcePsi.parent is PsiLanguageInjectionHost) { // Kotlin interpolated string, TODO: encapsulate this logic to range retrieval + return entry.range.shiftRight(sourcePsi.startOffsetInParent - ElementManipulators.getValueTextRange(sourcePsi.parent).startOffset) + } + return null + } +} + +@ApiStatus.Experimental +class PartiallyKnownString(val segments: List) { + + val valueIfKnown: String? + get() { + (segments.singleOrNull() as? StringEntry.Known)?.let { return it.value } + + val stringBuffer = StringBuffer() + for (segment in segments) { + when (segment) { + is StringEntry.Known -> stringBuffer.append(segment.value) + is StringEntry.Unknown -> return null + } + } + return stringBuffer.toString() + } + + override fun toString(): String = segments.joinToString { segment -> + when (segment) { + is StringEntry.Known -> segment.value + is StringEntry.Unknown -> "" + } + } + + constructor(single: StringEntry) : this(listOf(single)) + + constructor(string: String, sourcePsi: PsiElement?, textRange: TextRange) : this( + StringEntry.Known(string, sourcePsi, textRange)) + + fun findIndexOfInKnown(pattern: String): Int { + var accumulated = 0 + for (segment in segments) { + when (segment) { + is StringEntry.Known -> { + val i = segment.value.indexOf(pattern) + if (i >= 0) return accumulated + i + accumulated += segment.value.length + } + is StringEntry.Unknown -> { + } + } + } + return -1 + } + + fun splitAtInKnown(splitAt: Int): Pair { + var accumulated = 0 + val left = SmartList() + for ((i, segment) in segments.withIndex()) { + when (segment) { + is StringEntry.Known -> { + if (accumulated + segment.value.length < splitAt) { + accumulated += segment.value.length + left.add(segment) + } + else { + val leftPart = segment.value.substring(0, splitAt - accumulated) + val rightPart = segment.value.substring(splitAt - accumulated) + left.add(StringEntry.Known(leftPart, segment.sourcePsi, /* TODO: should also be splitted */ + segment.range)) + + return PartiallyKnownString(left) to PartiallyKnownString( + ArrayList(segments.lastIndex - i + 1).apply { + if (rightPart.isNotEmpty()) + add(StringEntry.Known(rightPart, segment.sourcePsi, /* TODO: should also be splitted */ + segment.range)) + addAll(segments.subList(i, segments.size)) + } + ) + } + } + is StringEntry.Unknown -> { + left.add(segment) + } + } + } + return this to empty + } + + fun split(pattern: String): List { + + tailrec fun collectPaths(result: MutableList, + pending: MutableList, + segments: List): MutableList { + + val (head, tail) = segments.toHeadAndTail() ?: return result.apply { + add( + PartiallyKnownString(pending)) + } + + when (head) { + is StringEntry.Unknown -> return collectPaths(result, pending.apply { add(head) }, tail) + is StringEntry.Known -> { + val value = head.value + + val stringPaths = splitToTextRanges(value, pattern).toList() + if (stringPaths.size == 1) { + return collectPaths(result, pending.apply { add(head) }, tail) + } + else { + return collectPaths( + result.apply { + add(PartiallyKnownString( + pending.apply { + add(StringEntry.Known(stringPaths.first().substring(value), head.sourcePsi, + stringPaths.first())) + })) + addAll(stringPaths.subList(1, stringPaths.size - 1).map { + PartiallyKnownString(it.substring(value), head.sourcePsi, it) + }) + }, + mutableListOf(StringEntry.Known(stringPaths.last().substring(value), head.sourcePsi, + stringPaths.last())), + tail + ) + } + + } + } + + } + + return collectPaths(SmartList(), mutableListOf(), segments) + + } + + companion object { + val empty = PartiallyKnownString(emptyList()) + } + +} + +@ApiStatus.Experimental +fun splitToTextRanges(charSequence: CharSequence, pattern: String): Sequence { + var lastMatch = 0 + return sequence { + while (true) { + val start = charSequence.indexOf(pattern, lastMatch) + if (start == -1) { + yield(TextRange(lastMatch, charSequence.length)) + return@sequence + } + yield(TextRange(lastMatch, start)) + lastMatch = start + pattern.length + } + } + +} \ No newline at end of file diff --git a/uast/uast-common/intellij.platform.uast.iml b/uast/uast-common/intellij.platform.uast.iml index 230a7cf5ba49..7b54a0989758 100644 --- a/uast/uast-common/intellij.platform.uast.iml +++ b/uast/uast-common/intellij.platform.uast.iml @@ -11,5 +11,6 @@ + \ No newline at end of file diff --git a/uast/uast-common/src/org/jetbrains/uast/expressions/UStringConcatenationsFacade.kt b/uast/uast-common/src/org/jetbrains/uast/expressions/UStringConcatenationsFacade.kt index 5892e88a9d26..5666aa32e0b0 100644 --- a/uast/uast-common/src/org/jetbrains/uast/expressions/UStringConcatenationsFacade.kt +++ b/uast/uast-common/src/org/jetbrains/uast/expressions/UStringConcatenationsFacade.kt @@ -5,8 +5,8 @@ import com.intellij.openapi.util.TextRange import com.intellij.psi.ElementManipulators import com.intellij.psi.PsiElement import com.intellij.psi.PsiLanguageInjectionHost -import com.intellij.util.SmartList -import com.intellij.util.containers.toHeadAndTail +import com.intellij.psi.util.PartiallyKnownString +import com.intellij.psi.util.StringEntry import org.jetbrains.annotations.ApiStatus import org.jetbrains.uast.* @@ -82,8 +82,8 @@ class UStringConcatenationsFacade @ApiStatus.Experimental constructor(uContext: @ApiStatus.Experimental fun asPartiallyKnownString() = PartiallyKnownString(segments.map { segment -> segment.value?.let { value -> - StringEntry.Known(value, segment.uExpression, getSegmentInnerTextRange(segment)) - } ?: StringEntry.Unknown(segment.uExpression, getSegmentInnerTextRange(segment)) + StringEntry.Known(value, segment.uExpression.sourcePsi, getSegmentInnerTextRange(segment)) + } ?: StringEntry.Unknown(segment.uExpression.sourcePsi, getSegmentInnerTextRange(segment)) }) private fun getSegmentInnerTextRange(segment: Segment): TextRange { @@ -108,160 +108,3 @@ class UStringConcatenationsFacade @ApiStatus.Experimental constructor(uContext: } -@ApiStatus.Experimental -sealed class StringEntry { - abstract val uExpression: UExpression - abstract val range: TextRange - - class Known(val value: String, override val uExpression: UExpression, override val range: TextRange) : StringEntry() - class Unknown(override val uExpression: UExpression, override val range: TextRange) : StringEntry() - - val rangeAlignedToHost: TextRange? - get() { - val entry = this - val sourcePsi = entry.uExpression.sourcePsi ?: return null - if (sourcePsi is PsiLanguageInjectionHost) return entry.range - if (sourcePsi.parent is PsiLanguageInjectionHost) { // Kotlin interpolated string, TODO: encapsulate this logic to range retrieval - return entry.range.shiftRight(sourcePsi.startOffsetInParent - ElementManipulators.getValueTextRange(sourcePsi.parent).startOffset) - } - return null - } -} - -@ApiStatus.Experimental -class PartiallyKnownString(val segments: List) { - - val valueIfKnown: String? - get() { - (segments.singleOrNull() as? StringEntry.Known)?.let { return it.value } - - val stringBuffer = StringBuffer() - for (segment in segments) { - when (segment) { - is StringEntry.Known -> stringBuffer.append(segment.value) - is StringEntry.Unknown -> return null - } - } - return stringBuffer.toString() - } - - override fun toString(): String = segments.joinToString { segment -> - when (segment) { - is StringEntry.Known -> segment.value - is StringEntry.Unknown -> "" - } - } - - constructor(single: StringEntry) : this(listOf(single)) - - constructor(string: String, uExpression: UExpression, textRange: TextRange) : this(StringEntry.Known(string, uExpression, textRange)) - - fun findIndexOfInKnown(pattern: String): Int { - var accumulated = 0 - for (segment in segments) { - when (segment) { - is StringEntry.Known -> { - val i = segment.value.indexOf(pattern) - if (i >= 0) return accumulated + i - accumulated += segment.value.length - } - is StringEntry.Unknown -> { - } - } - } - return -1 - } - - fun splitAtInKnown(splitAt: Int): Pair { - var accumulated = 0 - val left = SmartList() - for ((i, segment) in segments.withIndex()) { - when (segment) { - is StringEntry.Known -> { - if (accumulated + segment.value.length < splitAt) { - accumulated += segment.value.length - left.add(segment) - } - else { - val leftPart = segment.value.substring(0, splitAt - accumulated) - val rightPart = segment.value.substring(splitAt - accumulated) - left.add(StringEntry.Known(leftPart, segment.uExpression, /* TODO: should also be splitted */ segment.range)) - - return PartiallyKnownString(left) to PartiallyKnownString( - ArrayList(segments.lastIndex - i + 1).apply { - if (rightPart.isNotEmpty()) - add(StringEntry.Known(rightPart, segment.uExpression, /* TODO: should also be splitted */ segment.range)) - addAll(segments.subList(i, segments.size)) - } - ) - } - } - is StringEntry.Unknown -> { - left.add(segment) - } - } - } - return this to PartiallyKnownString.empty - } - - fun split(pattern: String): List { - - tailrec fun collectPaths(result: MutableList, - pending: MutableList, - segments: List): MutableList { - - val (head, tail) = segments.toHeadAndTail() ?: return result.apply { add(PartiallyKnownString(pending)) } - - when (head) { - is StringEntry.Unknown -> return collectPaths(result, pending.apply { add(head) }, tail) - is StringEntry.Known -> { - val value = head.value - - val stringPaths = splitToTextRanges(value, pattern).toList() - if (stringPaths.size == 1) { - return collectPaths(result, pending.apply { add(head) }, tail) - } - else { - return collectPaths( - result.apply { - add(PartiallyKnownString( - pending.apply { add(StringEntry.Known(stringPaths.first().substring(value), head.uExpression, stringPaths.first())) })) - addAll(stringPaths.subList(1, stringPaths.size - 1).map { PartiallyKnownString(it.substring(value), head.uExpression, it) }) - }, - mutableListOf(StringEntry.Known(stringPaths.last().substring(value), head.uExpression, stringPaths.last())), - tail - ) - } - - } - } - - } - - return collectPaths(SmartList(), mutableListOf(), segments) - - } - - companion object { - val empty = PartiallyKnownString(emptyList()) - } - -} - -@ApiStatus.Experimental -fun splitToTextRanges(charSequence: CharSequence, pattern: String): Sequence { - var lastMatch = 0 - return sequence { - while (true) { - val start = charSequence.indexOf(pattern, lastMatch) - if (start == -1) { - yield(TextRange(lastMatch, charSequence.length)) - return@sequence - } - yield(TextRange(lastMatch, start)) - lastMatch = start + pattern.length - } - } - -} -