Compare commits

...

2 Commits

7 changed files with 234 additions and 43 deletions

26
bin/deploy_plugin Executable file
View File

@ -0,0 +1,26 @@
#!/bin/sh
#
# Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
#
set -e
echo
echo "Creating plugin"
echo
./gradlew buildInstallablePlugin
deploy_site -u

View File

@ -1,7 +1,7 @@
#!/bin/bash #!/bin/bash
# #
# Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com # Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
@ -17,6 +17,14 @@
# #
# #
upload_only=false
for arg in "$@"; do
if [[ "$arg" == "-u" || "$arg" == "--upload-only" ]]; then
upload_only=true
break
fi
done
function checkState() { function checkState() {
if [[ $? != 0 ]]; then if [[ $? != 0 ]]; then
echo echo
@ -24,9 +32,10 @@ function checkState() {
echo echo
exit 100 exit 100
fi fi
} }
# Update docs/idea_plugin.md to point to the latest built IDEA plugin zip # Update docs/idea_plugin.md to point to the latest built IDEA plugin zip
# from ./distributables before building the site. The change is temporary and # from ./distributables before building the site. The change is temporary and
# the original file is restored right after the build. # the original file is restored right after the build.
@ -107,6 +116,8 @@ function refreshTextmateZip() {
(cd editors && zip -rq ../distributables/lyng-textmate.zip .) (cd editors && zip -rq ../distributables/lyng-textmate.zip .)
} }
if [[ "$upload_only" == false ]]; then
# Update the IDEA plugin download link in docs (temporarily), then build, then restore the doc # Update the IDEA plugin download link in docs (temporarily), then build, then restore the doc
refreshTextmateZip refreshTextmateZip
updateIdeaPluginDownloadLink || echo "WARN: proceeding without updating IDEA plugin download link" updateIdeaPluginDownloadLink || echo "WARN: proceeding without updating IDEA plugin download link"
@ -125,9 +136,7 @@ if [[ $BUILD_RC -ne 0 ]]; then
echo echo
exit 100 exit 100
fi fi
fi
#exit 0
# Prepare working dir # Prepare working dir
ssh -p ${SSH_PORT} ${SSH_HOST} " ssh -p ${SSH_PORT} ${SSH_HOST} "
@ -143,12 +152,15 @@ ssh -p ${SSH_PORT} ${SSH_HOST} "
fi fi
"; ";
if [[ "$upload_only" == false ]]; then
# sync files # sync files
SRC=./site/build/dist/js/productionExecutable SRC=./site/build/dist/js/productionExecutable
rsync -e "ssh -p ${SSH_PORT}" -avz -r -d --delete ${SRC}/* ${SSH_HOST}:${ROOT}/build/dist rsync -e "ssh -p ${SSH_PORT}" -avz -r -d --delete ${SRC}/* ${SSH_HOST}:${ROOT}/build/dist
checkState checkState
#rsync -e "ssh -p ${SSH_PORT}" -avz ./static/* ${SSH_HOST}:${ROOT}/build/dist #rsync -e "ssh -p ${SSH_PORT}" -avz ./static/* ${SSH_HOST}:${ROOT}/build/dist
#checkState #checkState
fi
rsync -e "ssh -p ${SSH_PORT}" -avz -r -d --delete distributables/* ${SSH_HOST}:${ROOT}/build/dist/distributables rsync -e "ssh -p ${SSH_PORT}" -avz -r -d --delete distributables/* ${SSH_HOST}:${ROOT}/build/dist/distributables
checkState checkState

View File

@ -518,18 +518,26 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
if (d.mutable) "var ${d.name}${typeStr}" else "val ${d.name}${typeStr}" if (d.mutable) "var ${d.name}${typeStr}" else "val ${d.name}${typeStr}"
} }
} }
// Show full detailed documentation, not just the summary
val raw = d.doc?.raw
val doc: String? = if (raw.isNullOrBlank()) null else MarkdownRenderer.render(raw)
val sb = StringBuilder() val sb = StringBuilder()
sb.append("<div class='doc-title'>").append(htmlEscape(title)).append("</div>") sb.append("<div class='doc-title'>").append(htmlEscape(title)).append("</div>")
if (!doc.isNullOrBlank()) sb.append(styledMarkdown(doc)) sb.append(renderDocBody(d.doc))
return sb.toString() return sb.toString()
} }
private fun renderParamDoc(fn: MiniFunDecl, p: MiniParam): String { private fun renderParamDoc(fn: MiniFunDecl, p: MiniParam): String {
val title = "parameter ${p.name}${typeOf(p.type)} in ${fn.name}${signatureOf(fn)}" val title = "parameter ${p.name}${typeOf(p.type)} in ${fn.name}${signatureOf(fn)}"
return "<div class='doc-title'>${htmlEscape(title)}</div>" val sb = StringBuilder()
sb.append("<div class='doc-title'>").append(htmlEscape(title)).append("</div>")
// Find matching @param tag
fn.doc?.tags?.get("param")?.forEach { tag ->
val parts = tag.split(Regex("\\s+"), 2)
if (parts.getOrNull(0) == p.name && parts.size > 1) {
sb.append(styledMarkdown(MarkdownRenderer.render(parts[1])))
}
}
return sb.toString()
} }
private fun renderMemberFunDoc(className: String, m: MiniMemberFunDecl): String { private fun renderMemberFunDoc(className: String, m: MiniMemberFunDecl): String {
@ -540,11 +548,9 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
val ret = typeOf(m.returnType) val ret = typeOf(m.returnType)
val staticStr = if (m.isStatic) "static " else "" val staticStr = if (m.isStatic) "static " else ""
val title = "${staticStr}method $className.${m.name}(${params})${ret}" val title = "${staticStr}method $className.${m.name}(${params})${ret}"
val raw = m.doc?.raw
val doc: String? = if (raw.isNullOrBlank()) null else MarkdownRenderer.render(raw)
val sb = StringBuilder() val sb = StringBuilder()
sb.append("<div class='doc-title'>").append(htmlEscape(title)).append("</div>") sb.append("<div class='doc-title'>").append(htmlEscape(title)).append("</div>")
if (!doc.isNullOrBlank()) sb.append(styledMarkdown(doc)) sb.append(renderDocBody(m.doc))
return sb.toString() return sb.toString()
} }
@ -553,11 +559,9 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
val kind = if (m.mutable) "var" else "val" val kind = if (m.mutable) "var" else "val"
val staticStr = if (m.isStatic) "static " else "" val staticStr = if (m.isStatic) "static " else ""
val title = "${staticStr}${kind} $className.${m.name}${ts}" val title = "${staticStr}${kind} $className.${m.name}${ts}"
val raw = m.doc?.raw
val doc: String? = if (raw.isNullOrBlank()) null else MarkdownRenderer.render(raw)
val sb = StringBuilder() val sb = StringBuilder()
sb.append("<div class='doc-title'>").append(htmlEscape(title)).append("</div>") sb.append("<div class='doc-title'>").append(htmlEscape(title)).append("</div>")
if (!doc.isNullOrBlank()) sb.append(styledMarkdown(doc)) sb.append(renderDocBody(m.doc))
return sb.toString() return sb.toString()
} }
@ -663,6 +667,55 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
return if (e > s) TextRange(s, e) else null return if (e > s) TextRange(s, e) else null
} }
private fun renderDocBody(doc: MiniDoc?): String {
if (doc == null) return ""
val sb = StringBuilder()
if (doc.raw.isNotBlank()) {
sb.append(styledMarkdown(MarkdownRenderer.render(doc.raw)))
}
if (doc.tags.isNotEmpty()) {
sb.append(renderTags(doc.tags))
}
return sb.toString()
}
private fun renderTags(tags: Map<String, List<String>>): String {
if (tags.isEmpty()) return ""
val sb = StringBuilder()
sb.append("<table class='sections'>")
fun section(title: String, list: List<String>, isKeyValue: Boolean = false) {
if (list.isEmpty()) return
sb.append("<tr><td valign='top' class='section'><p>").append(htmlEscape(title)).append(":</p></td><td valign='top'>")
list.forEachIndexed { index, item ->
if (index > 0) sb.append("<p>")
if (isKeyValue) {
val parts = item.split(Regex("\\s+"), 2)
sb.append("<code>").append(htmlEscape(parts[0])).append("</code>")
if (parts.size > 1) {
sb.append("").append(MarkdownRenderer.render(parts[1]).removePrefix("<p>").removeSuffix("</p>"))
}
} else {
sb.append(MarkdownRenderer.render(item).removePrefix("<p>").removeSuffix("</p>"))
}
}
sb.append("</td></tr>")
}
section("Parameters", tags["param"] ?: emptyList(), isKeyValue = true)
section("Returns", tags["return"] ?: emptyList())
section("Throws", tags["throws"] ?: emptyList(), isKeyValue = true)
tags.forEach { (name, list) ->
if (name !in listOf("param", "return", "throws")) {
section(name.replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() }, list)
}
}
sb.append("</table>")
return sb.toString()
}
private fun previousWordBefore(text: String, offset: Int): TextRange? { private fun previousWordBefore(text: String, offset: Int): TextRange? {
// skip spaces and the dot to the left, but stop after hitting a non-identifier boundary // skip spaces and the dot to the left, but stop after hitting a non-identifier boundary
var i = (offset - 1).coerceAtLeast(0) var i = (offset - 1).coerceAtLeast(0)

View File

@ -116,10 +116,8 @@ class Compiler(
private fun consumePendingDoc(): MiniDoc? { private fun consumePendingDoc(): MiniDoc? {
if (pendingDocLines.isEmpty()) return null if (pendingDocLines.isEmpty()) return null
val raw = pendingDocLines.joinToString("\n").trimEnd()
val summary = raw.lines().firstOrNull { it.isNotBlank() }?.trim()
val start = pendingDocStart ?: cc.currentPos() val start = pendingDocStart ?: cc.currentPos()
val doc = MiniDoc(MiniRange(start, start), raw = raw, summary = summary) val doc = MiniDoc.parse(MiniRange(start, start), pendingDocLines)
clearPendingDoc() clearPendingDoc()
return doc return doc
} }

View File

@ -255,8 +255,7 @@ class ClassDocsBuilder internal constructor(private val className: String) {
private fun builtinRange() = MiniRange(Pos.builtIn, Pos.builtIn) private fun builtinRange() = MiniRange(Pos.builtIn, Pos.builtIn)
private fun miniDoc(text: String, tags: Map<String, List<String>>): MiniDoc { private fun miniDoc(text: String, tags: Map<String, List<String>>): MiniDoc {
val summary = text.lineSequence().map { it.trim() }.firstOrNull { it.isNotEmpty() } return MiniDoc.parse(builtinRange(), listOf(text), tags)
return MiniDoc(range = builtinRange(), raw = text, summary = summary, tags = tags)
} }
private fun TypeDoc.toDisplayName(): String = when (this) { private fun TypeDoc.toDisplayName(): String = when (this) {

View File

@ -39,7 +39,56 @@ data class MiniDoc(
val raw: String, val raw: String,
val summary: String?, val summary: String?,
val tags: Map<String, List<String>> = emptyMap() val tags: Map<String, List<String>> = emptyMap()
) ) {
companion object {
fun parse(range: MiniRange, lines: Iterable<String>, extraTags: Map<String, List<String>> = emptyMap()): MiniDoc {
val parsedTags = mutableMapOf<String, MutableList<String>>()
var currentTag: String? = null
val currentContent = StringBuilder()
fun flush() {
currentTag?.let { tag ->
parsedTags.getOrPut(tag) { mutableListOf() }.add(currentContent.toString().trim())
}
currentContent.setLength(0)
}
val descriptionLines = mutableListOf<String>()
var inTags = false
for (rawLine in lines) {
for (line in rawLine.lines()) {
val trimmed = line.trim()
if (trimmed.startsWith("@")) {
inTags = true
flush()
val parts = trimmed.substring(1).split(Regex("\\s+"), 2)
currentTag = parts[0]
currentContent.append(parts.getOrNull(1) ?: "")
} else {
if (inTags) {
if (currentContent.isNotEmpty()) currentContent.append("\n")
currentContent.append(line)
} else {
descriptionLines.add(line)
}
}
}
}
flush()
val raw = descriptionLines.joinToString("\n").trimEnd()
val summary = raw.lines().firstOrNull { it.isNotBlank() }?.trim()
val finalTags = parsedTags.toMutableMap()
extraTags.forEach { (k, v) ->
finalTags.getOrPut(k) { mutableListOf() }.addAll(v)
}
return MiniDoc(range, raw, summary, finalTags)
}
}
}
sealed interface MiniNode { val range: MiniRange } sealed interface MiniNode { val range: MiniRange }

View File

@ -473,4 +473,58 @@ class MiniAstTest {
assertEquals("a", fn.params[0].name) assertEquals("a", fn.params[0].name)
assertEquals("b", fn.params[1].name) assertEquals("b", fn.params[1].name)
} }
@Test
fun miniAst_captures_dokka_tags() = runTest {
val code = """
/**
* Testing tags.
* @param x the x value
* @param y the y value
* @return some string
* @throws Exception if failed
*/
fun tagged(x: Int, y: Int): String { "" }
""".trimIndent()
val (_, sink) = compileWithMini(code)
val mini = sink.build()
assertNotNull(mini)
val fn = mini.declarations.filterIsInstance<MiniFunDecl>().firstOrNull { it.name == "tagged" }
assertNotNull(fn)
val doc = fn.doc
assertNotNull(doc)
assertEquals("Testing tags.", doc.summary)
val tags = doc.tags
assertTrue(tags.containsKey("param"), "should have @param tags")
assertEquals(listOf("x the x value", "y the y value"), tags["param"])
assertEquals(listOf("some string"), tags["return"])
assertEquals(listOf("Exception if failed"), tags["throws"])
}
@Test
fun miniAst_captures_multiline_tags() = runTest {
val code = """
/**
* Multi line tag.
* @param x first line of x
* second line of x
* @return return value
*/
fun multiline(x: Int): Int { 0 }
""".trimIndent()
val (_, sink) = compileWithMini(code)
val mini = sink.build()
assertNotNull(mini)
val fn = mini.declarations.filterIsInstance<MiniFunDecl>().firstOrNull { it.name == "multiline" }
assertNotNull(fn)
val doc = fn.doc
assertNotNull(doc)
val tags = doc.tags
assertTrue(tags.containsKey("param"), "should have @param tags")
val xParam = tags["param"]?.first() ?: ""
assertTrue(xParam.contains("first line of x"), "should contain first line")
assertTrue(xParam.contains("second line of x"), "should contain second line")
}
} }