summaryrefslogtreecommitdiff
path: root/platform/lang-impl/src/com/intellij/codeInsight/hints/InlayHintsUtils.kt
blob: 339666649422eef5dd3a1ade8b48d257409fb27f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
// Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.codeInsight.hints

import com.intellij.codeInsight.hints.presentation.AttributesTransformerPresentation
import com.intellij.codeInsight.hints.presentation.InlayPresentation
import com.intellij.codeInsight.hints.presentation.RecursivelyUpdatingRootPresentation
import com.intellij.codeInsight.hints.presentation.RootInlayPresentation
import com.intellij.configurationStore.deserializeInto
import com.intellij.configurationStore.serialize
import com.intellij.lang.Language
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.application.invokeLater
import com.intellij.openapi.editor.DefaultLanguageHighlighterColors
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.markup.EffectType
import com.intellij.openapi.editor.markup.TextAttributesEffectsBuilder
import com.intellij.openapi.util.TextRange
import com.intellij.psi.*
import com.intellij.refactoring.suggested.endOffset
import com.intellij.refactoring.suggested.startOffset
import com.intellij.util.SmartList
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.annotations.Nls
import org.jetbrains.annotations.Nls.Capitalization.Title
import java.awt.Dimension
import java.awt.Rectangle
import java.util.function.Supplier

class ProviderWithSettings<T: Any>(
  val info: ProviderInfo<T>,
  var settings: T
) {
  val configurable by lazy { provider.createConfigurable(settings) }

  val provider: InlayHintsProvider<T>
  get() = info.provider
  val language: Language
  get() = info.language
}

fun <T : Any> ProviderWithSettings<T>.withSettingsCopy(): ProviderWithSettings<T> {
  val settingsCopy = copySettings(settings, provider)
  return ProviderWithSettings(info, settingsCopy)
}

fun <T : Any> ProviderWithSettings<T>.getCollectorWrapperFor(file: PsiFile, editor: Editor, language: Language): CollectorWithSettings<T>? {
  val key = provider.key
  val sink = InlayHintsSinkImpl(editor)
  val collector = provider.getCollectorFor(file, editor, settings, sink) ?: return null
  return CollectorWithSettings(collector, key, language, sink)
}

internal fun <T : Any> ProviderWithSettings<T>.getPlaceholdersCollectorFor(file: PsiFile, editor: Editor): CollectorWithSettings<T>? {
  val key = provider.key
  val sink = InlayHintsSinkImpl(editor)
  val collector = provider.getPlaceholdersCollectorFor(file, editor, settings, sink) ?: return null

  return CollectorWithSettings(collector, key, language, sink)
}

internal fun <T : Any> InlayHintsProvider<T>.withSettings(language: Language, config: InlayHintsSettings): ProviderWithSettings<T> {
  val settings = getActualSettings(config, language)
  return ProviderWithSettings(ProviderInfo(language, this), settings)
}

internal fun <T : Any> InlayHintsProvider<T>.getActualSettings(config: InlayHintsSettings, language: Language): T =
  config.findSettings(key, language) { createSettings() }

internal fun <T : Any> copySettings(from: T, provider: InlayHintsProvider<T>): T {
  val settings = provider.createSettings()
  // Workaround to make a deep copy of settings. The other way is to parametrize T with something like
  // interface DeepCopyable<T> { fun deepCopy(from: T): T }, but there will be a lot of problems with recursive type bounds
  // That way was implemented and rejected
  serialize(from)?.deserializeInto(settings)
  return settings
}

internal fun strikeOutBuilder(editor: Editor): TextAttributesEffectsBuilder {
  val effectColor = editor.colorsScheme.getAttributes(DefaultLanguageHighlighterColors.INLAY_DEFAULT).foregroundColor
  return TextAttributesEffectsBuilder.create().coverWith(EffectType.STRIKEOUT, effectColor)
}

class CollectorWithSettings<T : Any>(
  val collector: InlayHintsCollector,
  val key: SettingsKey<T>,
  val language: Language,
  val sink: InlayHintsSinkImpl
) {
  fun collectHints(element: PsiElement, editor: Editor): Boolean {
    return collector.collect(element, editor, sink)
  }

  /**
   * Collects hints from the file and apply them to editor.
   * Doesn't expect other hints in editor.
   * Use only for settings preview.
   */
  fun collectTraversingAndApply(editor: Editor, file: PsiFile, enabled: Boolean) {
    val hintsBuffer = collectTraversing(editor, file, enabled)
    applyToEditor(file, editor, hintsBuffer)
  }

  /**
   * Same as [collectTraversingAndApply] but invoked on bg thread
   */
  fun collectTraversingAndApplyOnEdt(editor: Editor, file: PsiFile, enabled: Boolean) {
    val hintsBuffer = collectTraversing(editor, file, true)
    if (!enabled) {
      val builder = strikeOutBuilder(editor)
      addStrikeout(hintsBuffer.inlineHints, builder) { root, constraints -> HorizontalConstrainedPresentation(root, constraints) }
      addStrikeout(hintsBuffer.blockAboveHints, builder) { root, constraints -> BlockConstrainedPresentation(root, constraints) }
      addStrikeout(hintsBuffer.blockBelowHints, builder) { root, constraints -> BlockConstrainedPresentation(root, constraints) }
    }
    invokeLater { applyToEditor(file, editor, hintsBuffer) }
  }

  fun collectTraversing(editor: Editor, file: PsiFile, enabled: Boolean): HintsBuffer {
    if (enabled) {
      val traverser = SyntaxTraverser.psiTraverser(file)
      traverser.forEach {
        collectHints(it, editor)
      }
    }
    return sink.complete()
  }

  fun applyToEditor(file: PsiFile, editor: Editor, hintsBuffer: HintsBuffer) {
    InlayHintsPass.applyCollected(hintsBuffer, file, editor)
  }
}

internal fun <T: Any> addStrikeout(inlineHints: Int2ObjectOpenHashMap<MutableList<ConstrainedPresentation<*, T>>>,
                          builder: TextAttributesEffectsBuilder,
                          factory: (RootInlayPresentation<*>, T?) -> ConstrainedPresentation<*, T>
) {
  inlineHints.forEach {
    it.value.replaceAll { presentation ->
      val transformer = AttributesTransformerPresentation(presentation.root) { builder.applyTo(it) }
      val rootPresentation = RecursivelyUpdatingRootPresentation(transformer)
      factory(rootPresentation, presentation.constraints)
    }
  }
}

fun InlayPresentation.fireContentChanged() {
  fireContentChanged(Rectangle(width, height))
}

fun InlayPresentation.fireUpdateEvent(previousDimension: Dimension) {
  val current = dimension()
  if (previousDimension != current) {
    fireSizeChanged(previousDimension, current)
  }
  fireContentChanged()
}

fun InlayPresentation.dimension() = Dimension(width, height)

private typealias ConstrPresent<C> = ConstrainedPresentation<*, C>

@ApiStatus.Experimental
fun InlayHintsSink.addCodeVisionElement(editor: Editor, offset: Int, priority: Int, presentation: InlayPresentation) {
  val line = editor.document.getLineNumber(offset)
  val column = offset - editor.document.getLineStartOffset(line)
  val root = RecursivelyUpdatingRootPresentation(presentation)
  val constraints = BlockConstraints(false, priority, InlayGroup.CODE_VISION_GROUP.ordinal, column)

  addBlockElement(line, true, root, constraints)
}

object InlayHintsUtils {
  fun getDefaultInlayHintsProviderPopupActions(
    providerKey: SettingsKey<*>,
    providerName: Supplier<@Nls(capitalization = Title) String>
  ): List<AnAction> =
    listOf(
      DisableInlayHintsProviderAction(providerKey, providerName, false),
      ConfigureInlayHintsProviderAction(providerKey)
    )

  fun getDefaultInlayHintsProviderCasePopupActions(
    providerKey: SettingsKey<*>,
    providerName: Supplier<@Nls(capitalization = Title) String>,
    caseId: String,
    caseName: Supplier<@Nls(capitalization = Title) String>
  ): List<AnAction> =
    listOf(
      DisableInlayHintsProviderCaseAction(providerKey, providerName, caseId, caseName),
      DisableInlayHintsProviderAction(providerKey, providerName, true),
      ConfigureInlayHintsProviderAction(providerKey)
    )

  /**
   * Function updates list of old presentations with new list, taking into account priorities.
   * Both lists must be sorted.
   *
   * @return list of updated constrained presentations
   */
  fun <Constraint : Any> produceUpdatedRootList(
    new: List<ConstrPresent<Constraint>>,
    old: List<ConstrPresent<Constraint>>,
    comparator: Comparator<ConstrPresent<Constraint>>,
    editor: Editor,
    factory: InlayPresentationFactory
  ): List<ConstrPresent<Constraint>> {
    val updatedPresentations: MutableList<ConstrPresent<Constraint>> = SmartList()

    // TODO [roman.ivanov]
    //  this function creates new list anyway, even if nothing from old presentations got updated,
    //  which makes us update list of presentations on every update (which should be relatively rare!)
    //  maybe I should really create new list only in case when anything get updated
    val oldSize = old.size
    val newSize = new.size
    var oldIndex = 0
    var newIndex = 0
    // Simultaneous bypass of both lists and merging them to new one with element update
    loop@
    while (true) {
      val newEl = new[newIndex]
      val oldEl = old[oldIndex]
      val value = comparator.compare(newEl, oldEl)
      when {
        value > 0 -> {
          oldIndex++
          if (oldIndex == oldSize) {
            break@loop
          }
        }
        value < 0 -> {
          updatedPresentations.add(newEl)
          newIndex++
          if (newIndex == newSize) {
            break@loop
          }
        }
        else -> {
          val oldRoot = oldEl.root
          val newRoot = newEl.root

          if (newRoot.key == oldRoot.key) {
            oldRoot.updateIfSame(newRoot, editor, factory)
            updatedPresentations.add(oldEl)
          }
          else {
            updatedPresentations.add(newEl)
          }
          newIndex++
          oldIndex++
          if (newIndex == newSize || oldIndex == oldSize) {
            break@loop
          }
        }
      }
    }
    for (i in newIndex until newSize) {
      updatedPresentations.add(new[i])
    }
    return updatedPresentations
  }

  /**
   * @return true iff updated
   */
  private fun <Content : Any>RootInlayPresentation<Content>.updateIfSame(
    newPresentation: RootInlayPresentation<*>,
    editor: Editor,
    factory: InlayPresentationFactory
  ) : Boolean {
    if (key != newPresentation.key) return false
    @Suppress("UNCHECKED_CAST")
    return update(newPresentation.content as Content, editor, factory)
  }

  /**
   * Note that the range may still be invalid if document doesn't match PSI
   */
  fun getTextRangeWithoutLeadingCommentsAndWhitespaces(element: PsiElement): TextRange {
    val start = SyntaxTraverser.psiApi().children(element).firstOrNull { it !is PsiComment && it !is PsiWhiteSpace } ?: element

    return TextRange.create(start.startOffset, element.endOffset)
  }

  @JvmStatic
  fun isFirstInLine(element: PsiElement): Boolean {
    val prevSibling = element.prevSibling
    return prevSibling is PsiWhiteSpace &&
           (prevSibling.textContains('\n') || prevSibling.getTextRange().startOffset == 0) ||
           element.textRange.startOffset == 0
  }
}