diff --git a/bin/deploy_site b/bin/deploy_site index eebe406..57c107a 100755 --- a/bin/deploy_site +++ b/bin/deploy_site @@ -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 diff --git a/distributables/lyng-idea-0.0.3-SNAPSHOT.zip b/distributables/lyng-idea-0.0.3-SNAPSHOT.zip new file mode 100644 index 0000000..9c31d9b --- /dev/null +++ b/distributables/lyng-idea-0.0.3-SNAPSHOT.zip @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7079233ef1d30b371c4ea197e326d7b332e68eec584bdc239cad8ea377cab307 +size 27120267 diff --git a/lyng-idea/build.gradle.kts b/lyng-idea/build.gradle.kts index 25fd54b..defb653 100644 --- a/lyng-idea/build.gradle.kts +++ b/lyng-idea/build.gradle.kts @@ -21,7 +21,7 @@ plugins { } group = "net.sergeych.lyng" -version = "0.0.2-SNAPSHOT" +version = "0.0.3-SNAPSHOT" kotlin { jvmToolchain(17) diff --git a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/annotators/LyngExternalAnnotator.kt b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/annotators/LyngExternalAnnotator.kt index d1c15e2..77f2147 100644 --- a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/annotators/LyngExternalAnnotator.kt +++ b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/annotators/LyngExternalAnnotator.kt @@ -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() { - data class Input(val text: String, val modStamp: Long) + data class Input(val text: String, val modStamp: Long, val previousSpans: List?) 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) + data class Error(val start: Int, val end: Int, val message: String) + data class Result(val modStamp: Long, val spans: List, 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", text) try { // Call suspend API from blocking context - val src = Source("", 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("", text) + val mini = sink.build() ?: return Result(collectedInfo.modStamp, collectedInfo.previousSpans ?: emptyList()) val out = ArrayList(256) @@ -227,7 +240,7 @@ class LyngExternalAnnotator : ExternalAnnotator start) { + holder.newAnnotation(HighlightSeverity.ERROR, err.message) + .range(TextRange(start, end)) + .create() + } + } } companion object { private val CACHE_KEY: Key = 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 { + 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 + } } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/stdlib_included/root_lyng.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/stdlib_included/root_lyng.kt index 68fa82b..795b210 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/stdlib_included/root_lyng.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/stdlib_included/root_lyng.kt @@ -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() )