idea plugin now shows errors that prevent syntax highlighting and show cached syntax coloring remembered from the last successive pass

This commit is contained in:
Sergey Chernov 2025-12-02 03:29:14 +01:00
parent 067970b80c
commit fbea13570e
5 changed files with 134 additions and 15 deletions

View File

@ -27,6 +27,58 @@ function checkState() {
}
# 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
# the original file is restored right after the build.
DOC_IDEA_PLUGIN="docs/idea_plugin.md"
DOC_IDEA_PLUGIN_BACKUP="${DOC_IDEA_PLUGIN}.deploy_backup"
function updateIdeaPluginDownloadLink() {
if [[ ! -f "$DOC_IDEA_PLUGIN" ]]; then
echo "WARN: $DOC_IDEA_PLUGIN not found; skipping plugin link update"
return 0
fi
# Find the most recently modified plugin zip
local latest
latest=$(ls -t distributables/lyng-idea-*.zip 2>/dev/null | head -n 1)
if [[ -z "$latest" ]]; then
echo "WARN: no distributables/lyng-idea-*.zip found; leaving $DOC_IDEA_PLUGIN unchanged"
return 0
fi
local base
base=$(basename "$latest")
local version
version="${base#lyng-idea-}"
version="${version%.zip}"
local url
url="https://lynglang.com/distributables/${base}"
local newline
newline="### [Download plugin v${version}](${url})"
# Backup and rewrite the specific markdown line if present
cp "$DOC_IDEA_PLUGIN" "$DOC_IDEA_PLUGIN_BACKUP" || {
echo "ERROR: can't backup $DOC_IDEA_PLUGIN"; return 1; }
# Replace the line that starts with the download header; if not found, append it
awk -v repl="$newline" 'BEGIN{done=0} \
/^### \[Download plugin v/ { print repl; done=1; next } \
{ print } \
END { if (done==0) exit 42 }' "$DOC_IDEA_PLUGIN_BACKUP" > "$DOC_IDEA_PLUGIN"
local rc=$?
if [[ $rc -eq 42 ]]; then
echo "WARN: download link not found in $DOC_IDEA_PLUGIN; appending generated link"
echo >> "$DOC_IDEA_PLUGIN"
echo "$newline" >> "$DOC_IDEA_PLUGIN"
elif [[ $rc -ne 0 ]]; then
echo "ERROR: failed to update $DOC_IDEA_PLUGIN; restoring original"
mv -f "$DOC_IDEA_PLUGIN_BACKUP" "$DOC_IDEA_PLUGIN" 2>/dev/null
return 1
fi
}
# default target settings
case "com" in
com)
@ -46,16 +98,25 @@ esac
die() { echo "ERROR: $*" 1>&2 ; exit 1; }
#./gradlew site:clean site:jsBrowserDistribution || die "compilation failed"
./gradlew site:clean site:jsBrowserDistribution || die "compilation failed"
# Update the IDEA plugin download link in docs (temporarily), then build, then restore the doc
updateIdeaPluginDownloadLink || echo "WARN: proceeding without updating IDEA plugin download link"
if [[ $? != 0 ]]; then
./gradlew site:clean site:jsBrowserDistribution
BUILD_RC=$?
# Always restore original doc if backup exists
if [[ -f "$DOC_IDEA_PLUGIN_BACKUP" ]]; then
mv -f "$DOC_IDEA_PLUGIN_BACKUP" "$DOC_IDEA_PLUGIN"
fi
if [[ $BUILD_RC -ne 0 ]]; then
echo
echo -- build failed. deploy aborted.
echo
exit 100
fi
#exit 0
# Prepare working dir

BIN
distributables/lyng-idea-0.0.3-SNAPSHOT.zip (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -21,7 +21,7 @@ plugins {
}
group = "net.sergeych.lyng"
version = "0.0.2-SNAPSHOT"
version = "0.0.3-SNAPSHOT"
kotlin {
jvmToolchain(17)

View File

@ -27,6 +27,7 @@ import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiFile
import kotlinx.coroutines.runBlocking
import net.sergeych.lyng.Compiler
import net.sergeych.lyng.ScriptError
import net.sergeych.lyng.Source
import net.sergeych.lyng.binding.Binder
import net.sergeych.lyng.binding.SymbolKind
@ -42,14 +43,16 @@ import net.sergeych.lyng.miniast.*
* and applies semantic highlighting comparable with the web highlighter.
*/
class LyngExternalAnnotator : ExternalAnnotator<LyngExternalAnnotator.Input, LyngExternalAnnotator.Result>() {
data class Input(val text: String, val modStamp: Long)
data class Input(val text: String, val modStamp: Long, val previousSpans: List<Span>?)
data class Span(val start: Int, val end: Int, val key: com.intellij.openapi.editor.colors.TextAttributesKey)
data class Result(val modStamp: Long, val spans: List<Span>)
data class Error(val start: Int, val end: Int, val message: String)
data class Result(val modStamp: Long, val spans: List<Span>, val error: Error? = null)
override fun collectInformation(file: PsiFile): Input? {
val doc: Document = file.viewProvider.document ?: return null
return Input(doc.text, doc.modificationStamp)
val prev = file.getUserData(CACHE_KEY)?.spans
return Input(doc.text, doc.modificationStamp, prev)
}
override fun doAnnotate(collectedInfo: Input?): Result? {
@ -58,19 +61,29 @@ class LyngExternalAnnotator : ExternalAnnotator<LyngExternalAnnotator.Input, Lyn
val text = collectedInfo.text
// Build Mini-AST using the same mechanism as web highlighter
val sink = MiniAstBuilder()
val source = Source("<ide>", text)
try {
// Call suspend API from blocking context
val src = Source("<ide>", text)
val provider = IdeLenientImportProvider.create()
runBlocking { Compiler.compileWithMini(src, provider, sink) }
runBlocking { Compiler.compileWithMini(source, provider, sink) }
} catch (e: Throwable) {
if (e is com.intellij.openapi.progress.ProcessCanceledException) throw e
// Fail softly: no semantic layer this pass
return Result(collectedInfo.modStamp, emptyList())
// On script parse error: keep previous spans and report the error location
if (e is ScriptError) {
val off = try { source.offsetOf(e.pos) } catch (_: Throwable) { -1 }
val start0 = off.coerceIn(0, text.length.coerceAtLeast(0))
val (start, end) = expandErrorRange(text, start0)
return Result(
collectedInfo.modStamp,
collectedInfo.previousSpans ?: emptyList(),
Error(start, end, e.errorMessage)
)
}
// Other failures: keep previous spans without error
return Result(collectedInfo.modStamp, collectedInfo.previousSpans ?: emptyList(), null)
}
ProgressManager.checkCanceled()
val mini = sink.build() ?: return Result(collectedInfo.modStamp, emptyList())
val source = Source("<ide>", text)
val mini = sink.build() ?: return Result(collectedInfo.modStamp, collectedInfo.previousSpans ?: emptyList())
val out = ArrayList<Span>(256)
@ -227,7 +240,7 @@ class LyngExternalAnnotator : ExternalAnnotator<LyngExternalAnnotator.Input, Lyn
if (e is com.intellij.openapi.progress.ProcessCanceledException) throw e
}
return Result(collectedInfo.modStamp, out)
return Result(collectedInfo.modStamp, out, null)
}
override fun apply(file: PsiFile, annotationResult: Result?, holder: AnnotationHolder) {
@ -245,9 +258,48 @@ class LyngExternalAnnotator : ExternalAnnotator<LyngExternalAnnotator.Input, Lyn
.textAttributes(s.key)
.create()
}
// Show syntax error if present
val err = result.error
if (err != null) {
val start = err.start.coerceIn(0, (doc?.textLength ?: 0))
val end = err.end.coerceIn(start, (doc?.textLength ?: start))
if (end > start) {
holder.newAnnotation(HighlightSeverity.ERROR, err.message)
.range(TextRange(start, end))
.create()
}
}
}
companion object {
private val CACHE_KEY: Key<Result> = Key.create("LYNG_SEMANTIC_CACHE")
}
/**
* Make the error highlight a bit wider than a single character so it is easier to see and click.
* Strategy:
* - If the offset points inside an identifier-like token (letters/digits/underscore), expand to the full token.
* - Otherwise select a small range starting at the offset with a minimum width, but not crossing the line end.
*/
private fun expandErrorRange(text: String, rawStart: Int): Pair<Int, Int> {
if (text.isEmpty()) return 0 to 0
val len = text.length
val start = rawStart.coerceIn(0, len)
fun isWord(ch: Char) = ch == '_' || ch.isLetterOrDigit()
if (start < len && isWord(text[start])) {
var s = start
var e = start
while (s > 0 && isWord(text[s - 1])) s--
while (e < len && isWord(text[e])) e++
return s to e
}
// Not inside a word: select a short, visible range up to EOL
val lineEnd = text.indexOf('\n', start).let { if (it == -1) len else it }
val minWidth = 4
val end = (start + minWidth).coerceAtMost(lineEnd).coerceAtLeast((start + 1).coerceAtMost(lineEnd))
return start to end
}
}

View File

@ -124,7 +124,7 @@ fun Iterable.sumOf(f) {
}
fun Iterable.minOf( lambda ) {
val i = iterator()
val i = iterator()
var minimum = lambda( i.next() )
while( i.hasNext() ) {
val x = lambda(i.next())
@ -133,6 +133,9 @@ val i = iterator()
minimum
}
/*
Return maximum value of the given function applied to elements of the collection.
*/
fun Iterable.maxOf( lambda ) {
val i = iterator()
var maximum = lambda( i.next() )