expandable definition support

IJPL-156 Implement improvements for Quick documentation popup

GitOrigin-RevId: c4036fa30ccfe96c7742e95d1f383ff666432930
This commit is contained in:
Dmitry Avdeev
2023-10-19 17:18:39 +02:00
committed by intellij-monorepo-bot
parent 043e0e2665
commit bf6f9730f4
7 changed files with 98 additions and 5 deletions

View File

@@ -9,6 +9,7 @@ import com.intellij.openapi.util.text.HtmlChunk;
*/
public interface DocumentationMarkup {
@NlsSafe String DEFINITION_START = "<div class='definition'><pre>";
@NlsSafe String EXPANDABLE_DEFINITION_START = "<div class='definition expandable'><pre>";
@NlsSafe String DEFINITION_END = "</pre></div>";
@NlsSafe String CONTENT_START = "<div class='content'>";
@NlsSafe String CONTENT_END = "</div>";

View File

@@ -594,3 +594,5 @@ command.title.finishing.template=Finishing Template
notification.group.batch.quick.fix=Batch quick fix
command.check.availability.for=Check Availability for {0}
dialog.title.searching.for.usages=Searching for Usages
show.more=Show more
show.less=Show less

View File

@@ -110,7 +110,7 @@ internal class DocumentationBrowser private constructor(
private suspend fun handleLink(url: String) {
val targetPointer = this.targetPointer
val internalResult = try {
handleLink(project, targetPointer, url)
handleLink(project, targetPointer, url, page)
}
catch (e: IndexNotReadyException) {
return // normal situation, nothing to do

View File

@@ -3,8 +3,10 @@
package com.intellij.lang.documentation.ide.impl
import com.intellij.lang.documentation.ide.ui.ExpandableDefinition
import com.intellij.lang.documentation.ide.ui.UISnapshot
import com.intellij.lang.documentation.ide.ui.UIState
import com.intellij.lang.documentation.ide.ui.parseExpandableDefinition
import com.intellij.openapi.project.IndexNotReadyException
import com.intellij.platform.backend.documentation.ContentUpdater
import com.intellij.platform.backend.documentation.DocumentationContentData
@@ -19,6 +21,7 @@ internal class DocumentationPage(val request: DocumentationRequest) {
private val myContentFlow = MutableStateFlow<DocumentationPageContent?>(null)
val contentFlow: SharedFlow<DocumentationPageContent?> = myContentFlow.asSharedFlow()
val currentContent: DocumentationPageContent.Content? get() = myContentFlow.value as? DocumentationPageContent.Content
var expandableDefinition: ExpandableDefinition? = null
/**
* @return `true` if some content was loaded, `false` if content is empty
@@ -39,7 +42,9 @@ internal class DocumentationPage(val request: DocumentationRequest) {
return
}
val uiState = data.anchor?.let(UIState::ScrollToAnchor) ?: UIState.Reset
myContentFlow.value = prepareContent(data.content, data.links, uiState)
val original = data.content.html
expandableDefinition = parseExpandableDefinition(original, 4)
myContentFlow.value = prepareContent(data.content.copy(html = expandableDefinition?.getDecorated() ?: original), data.links, uiState)
update(data.updates, data.links)
}

View File

@@ -8,12 +8,14 @@ import com.intellij.codeInsight.documentation.DocumentationManagerProtocol
import com.intellij.ide.BrowserUtil
import com.intellij.lang.documentation.CompositeDocumentationProvider
import com.intellij.lang.documentation.ExternalDocumentationHandler
import com.intellij.lang.documentation.ide.ui.TOGGLE_EXPANDABLE_DEFINITION
import com.intellij.lang.documentation.psi.PsiElementDocumentationTarget
import com.intellij.model.Pointer
import com.intellij.openapi.application.readAction
import com.intellij.openapi.project.Project
import com.intellij.openapi.roots.OrderEntry
import com.intellij.platform.backend.documentation.DocumentationTarget
import com.intellij.platform.backend.documentation.impl.InternalLinkResult
import com.intellij.platform.backend.documentation.impl.handleLink
import com.intellij.psi.PsiManager
import com.intellij.util.SlowOperations
@@ -25,11 +27,19 @@ internal suspend fun handleLink(
project: Project,
targetPointer: Pointer<out DocumentationTarget>,
url: String,
page: DocumentationPage
): Any? {
if (url.startsWith("open")) {
return libraryEntry(project, targetPointer)
when {
url.startsWith("open") -> {
return libraryEntry(project, targetPointer)
}
url == TOGGLE_EXPANDABLE_DEFINITION -> {
val expandableDefinition = page.expandableDefinition!!
expandableDefinition.toggleExpanded()
return InternalLinkResult.Updater(expandableDefinition)
}
else -> return handleLink(targetPointer, url)
}
return handleLink(targetPointer, url)
}
private suspend fun libraryEntry(project: Project, targetPointer: Pointer<out DocumentationTarget>): OrderEntry? {

View File

@@ -0,0 +1,46 @@
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.lang.documentation.ide.ui
import com.intellij.lang.LangBundle
import com.intellij.lang.documentation.DocumentationMarkup.*
import com.intellij.openapi.util.text.HtmlChunk
import com.intellij.platform.backend.documentation.ContentUpdater
import kotlinx.coroutines.flow.flowOf
fun parseExpandableDefinition(doc: String, maxLines: Int): ExpandableDefinition? {
val start = doc.indexOf(EXPANDABLE_DEFINITION_START)
if (start < 0) return null
val end = doc.indexOf(DEFINITION_END)
if (end == -1) return null
return ExpandableDefinition(doc.substring(0, start),
doc.substring(start + EXPANDABLE_DEFINITION_START.length, end),
doc.substring(end + DEFINITION_END.length), maxLines)
}
const val TOGGLE_EXPANDABLE_DEFINITION = "toggle.expandable.definition"
class ExpandableDefinition(private val prefix: String, definition: String, private val content: String, maxLines: Int): ContentUpdater {
private val variants: Pair<String, String> = createVariants(definition, maxLines)
private var expanded = false
fun getDecorated(): String = prefix + DEFINITION_START + (if (expanded) variants.second else variants.first) + DEFINITION_END + content
fun toggleExpanded() {
expanded = !expanded
}
private fun createVariants(definition: String, maxLines: Int): Pair<String, String> {
var offset = 0
for (i in 0..<maxLines) {
offset = definition.indexOf("<br>", offset)
if (offset == -1) {
return Pair(definition, definition)
}
offset += "<br>".length
}
val collapsed = definition.substring(0, offset) + HtmlChunk.link(TOGGLE_EXPANDABLE_DEFINITION, LangBundle.message("show.more"))
val full = definition + HtmlChunk.br() + HtmlChunk.link(TOGGLE_EXPANDABLE_DEFINITION, LangBundle.message("show.less"))
return Pair(collapsed, full)
}
override fun prepareContentUpdates(currentContent: String) = flowOf(getDecorated())
}

View File

@@ -0,0 +1,29 @@
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.codeInsight.documentation
import com.intellij.lang.documentation.ide.ui.parseExpandableDefinition
import junit.framework.TestCase
import kotlin.test.assertNotEquals
class ExpandableDefinitionTest: TestCase() {
fun testShort() {
val short = "<div class='content-only'><div class='definition expandable'><pre><span style=\"color:#cf8e6d;\">interface&#32;</span><span style=\"\">Box</span> <span style=\"\">{<br></span><span style=\"\">&#32;&#32;&#32;&#32;scale:&#32;</span><span style=\"color:#cf8e6d;\">number</span><span style=\"\">,<br></span><span style=\"\">&#32;&#32;&#32;&#32;x:&#32;</span><span style=\"color:#cf8e6d;\">number<br></span><span style=\"\">}</span></pre></div><table class='sections'><tr><td valign='top'><icon src='JavaScriptPsiIcons.FileTypes.TypeScriptFile'/>&nbsp;src/foo/test.ts</td></table></div><div class=\"bottom\"><icon src=\"0\"/>&nbsp;TestProject</div>"
val definition = parseExpandableDefinition(short, 4)!!
assertEquals(
"<div class='content-only'><div class='definition'><pre><span style=\"color:#cf8e6d;\">interface&#32;</span><span style=\"\">Box</span> <span style=\"\">{<br></span><span style=\"\">&#32;&#32;&#32;&#32;scale:&#32;</span><span style=\"color:#cf8e6d;\">number</span><span style=\"\">,<br></span><span style=\"\">&#32;&#32;&#32;&#32;x:&#32;</span><span style=\"color:#cf8e6d;\">number<br></span><span style=\"\">}</span></pre></div><table class='sections'><tr><td valign='top'><icon src='JavaScriptPsiIcons.FileTypes.TypeScriptFile'/>&nbsp;src/foo/test.ts</td></table></div><div class=\"bottom\"><icon src=\"0\"/>&nbsp;TestProject</div>",
definition.getDecorated())
}
fun testCollapsedExpanded() {
val definition = parseExpandableDefinition(
"<div class='content-only'><div class='definition expandable'><pre><span style=\"color:#cf8e6d;\">interface&#32;</span><span style=\"\">Box</span> <span style=\"\">{<br></span><span style=\"\">&#32;&#32;&#32;&#32;scale:&#32;</span><span style=\"color:#cf8e6d;\">number</span><span style=\"\">,<br></span><span style=\"\">&#32;&#32;&#32;&#32;x:&#32;</span><span style=\"color:#cf8e6d;\">number</span><span style=\"\">,<br></span><span style=\"\">&#32;&#32;&#32;&#32;y:&#32;</span><span style=\"color:#cf8e6d;\">number</span><span style=\"\">,<br></span><span style=\"\">&#32;&#32;&#32;&#32;z:&#32;</span><span style=\"color:#cf8e6d;\">number<br></span><span style=\"\">}</span></pre></div><table class='sections'><tr><td valign='top'><icon src='JavaScriptPsiIcons.FileTypes.TypeScriptFile'/>&nbsp;src/foo/test.ts</td></table></div><div class=\"bottom\"><icon src=\"0\"/>&nbsp;TestProject</div>",
4)!!
assertNotNull(definition)
val collapsed = "<div class='content-only'><div class='definition'><pre><span style=\"color:#cf8e6d;\">interface&#32;</span><span style=\"\">Box</span> <span style=\"\">{<br></span><span style=\"\">&#32;&#32;&#32;&#32;scale:&#32;</span><span style=\"color:#cf8e6d;\">number</span><span style=\"\">,<br></span><span style=\"\">&#32;&#32;&#32;&#32;x:&#32;</span><span style=\"color:#cf8e6d;\">number</span><span style=\"\">,<br></span><span style=\"\">&#32;&#32;&#32;&#32;y:&#32;</span><span style=\"color:#cf8e6d;\">number</span><span style=\"\">,<br><a href=\"toggle.expandable.definition\">Show more</a></pre></div><table class='sections'><tr><td valign='top'><icon src='JavaScriptPsiIcons.FileTypes.TypeScriptFile'/>&nbsp;src/foo/test.ts</td></table></div><div class=\"bottom\"><icon src=\"0\"/>&nbsp;TestProject</div>"
assertEquals(collapsed, definition.getDecorated())
definition.toggleExpanded()
assertNotEquals(collapsed, definition.getDecorated())
val expanded = "<div class='content-only'><div class='definition'><pre><span style=\"color:#cf8e6d;\">interface&#32;</span><span style=\"\">Box</span> <span style=\"\">{<br></span><span style=\"\">&#32;&#32;&#32;&#32;scale:&#32;</span><span style=\"color:#cf8e6d;\">number</span><span style=\"\">,<br></span><span style=\"\">&#32;&#32;&#32;&#32;x:&#32;</span><span style=\"color:#cf8e6d;\">number</span><span style=\"\">,<br></span><span style=\"\">&#32;&#32;&#32;&#32;y:&#32;</span><span style=\"color:#cf8e6d;\">number</span><span style=\"\">,<br></span><span style=\"\">&#32;&#32;&#32;&#32;z:&#32;</span><span style=\"color:#cf8e6d;\">number<br></span><span style=\"\">}</span><br/><a href=\"toggle.expandable.definition\">Show less</a></pre></div><table class='sections'><tr><td valign='top'><icon src='JavaScriptPsiIcons.FileTypes.TypeScriptFile'/>&nbsp;src/foo/test.ts</td></table></div><div class=\"bottom\"><icon src=\"0\"/>&nbsp;TestProject</div>"
assertEquals(expanded, definition.getDecorated())
}
}