diff --git a/docs/whats_new.md b/docs/whats_new.md new file mode 100644 index 0000000..5077025 --- /dev/null +++ b/docs/whats_new.md @@ -0,0 +1,110 @@ +# What's New in Lyng + +This document highlights the latest additions and improvements to the Lyng language and its ecosystem. + +## Language Features + +### Class Properties with Accessors +Classes now support properties with custom `get()` and `set()` accessors. Properties in Lyng do **not** have automatic backing fields; they are pure accessors. + +```lyng +class Person(private var _age: Int) { + // Read-only property + val ageCategory get() = if (_age < 18) "Minor" else "Adult" + + // Read-write property + var age: Int + get() = _age + set(v) { + if (v >= 0) _age = v + } +} +``` + +### Private and Protected Setters +You can now restrict the visibility of a property's or field's setter using `private set` or `protected set`. This allows members to be publicly readable but only writable from within the declaring class or its subclasses. + +```lyng +class Counter { + var count = 0 + private set // Field with private setter + + fun increment() { count++ } +} + +class AdvancedCounter : Counter { + var totalOperations = 0 + protected set // Settable here and in further subclasses +} + +let c = Counter() +c.increment() // OK +// c.count = 10 // Error: setter is private +``` + +### Late-initialized `val` Fields +`val` fields in classes can be declared without an immediate initializer, provided they are assigned exactly once. If accessed before initialization, they hold the special `Unset` singleton. + +```lyng +class Service { + val logger + + fun check() { + if (logger == Unset) println("Not initialized yet") + } + + init { + logger = Logger("Service") + } +} +``` + +### Named Arguments and Named Splats +Function calls now support named arguments using the `name: value` syntax. If the variable name matches the parameter name, you can use the `name:` shorthand. + +```lyng +fun greet(name, greeting = "Hello") { + println("$greeting, $name!") +} + +val name = "Alice" +greet(name:) // Shorthand for greet(name: name) +greet(greeting: "Hi", name: "Bob") + +let params = { name: "Charlie", greeting: "Hey") +greet(...params) // Named splat expansion +``` + +### Multiple Inheritance (MI) +Lyng now supports multiple inheritance using the C3 Method Resolution Order (MRO). Use `this@Type` or casts for disambiguation. + +```lyng +class A { fun foo() = "A" } +class B { fun foo() = "B" } + +class Derived : A, B { + fun test() { + println(foo()) // Resolves to A.foo (leftmost) + println(this@B.foo()) // Qualified dispatch to B.foo + } +} + +let d = Derived() +println((d as B).foo()) // Disambiguation via cast +``` + +## Tooling and Infrastructure + +### CLI: Formatting Command +A new `fmt` subcommand has been added to the Lyng CLI. + +```bash +lyng fmt MyFile.lyng # Print formatted code to stdout +lyng fmt --in-place MyFile.lyng # Format file in-place +lyng fmt --check MyFile.lyng # Check if file needs formatting +``` + +### IDEA Plugin: Autocompletion +Experimental lightweight autocompletion is now available in the IntelliJ plugin. It features type-aware member suggestions and inheritance-aware completion. + +You can enable it in **Settings | Lyng Formatter | Enable Lyng autocompletion**. diff --git a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/completion/LyngCompletionContributor.kt b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/completion/LyngCompletionContributor.kt index 9c0bf84..16a3b6b 100644 --- a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/completion/LyngCompletionContributor.kt +++ b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/completion/LyngCompletionContributor.kt @@ -21,7 +21,6 @@ */ package net.sergeych.lyng.idea.completion -import LyngAstManager import com.intellij.codeInsight.completion.* import com.intellij.codeInsight.lookup.LookupElementBuilder import com.intellij.icons.AllIcons @@ -36,6 +35,7 @@ import net.sergeych.lyng.idea.LyngLanguage import net.sergeych.lyng.idea.highlight.LyngTokenTypes import net.sergeych.lyng.idea.settings.LyngFormatterSettings import net.sergeych.lyng.idea.util.DocsBootstrap +import net.sergeych.lyng.idea.util.LyngAstManager import net.sergeych.lyng.idea.util.TextCtx import net.sergeych.lyng.miniast.* diff --git a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/editor/LyngEnterHandler.kt b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/editor/LyngEnterHandler.kt index 9136ea3..0e1d999 100644 --- a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/editor/LyngEnterHandler.kt +++ b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/editor/LyngEnterHandler.kt @@ -85,7 +85,13 @@ class LyngEnterHandler : EnterHandlerDelegate { if (code == "}" || code == "*/") { // Adjust indent for the previous line if it's a block or comment closer val prevStart = doc.getLineStartOffset(prevLine) - CodeStyleManager.getInstance(project).adjustLineIndent(file, prevStart) + if (file.context == null) { + try { + CodeStyleManager.getInstance(project).adjustLineIndent(file, prevStart) + } catch (e: Exception) { + log.warn("Failed to adjust line indent for previous line: ${e.message}") + } + } // Fallback for previous line: manual application val desiredPrev = computeDesiredIndent(project, doc, prevLine) @@ -102,8 +108,13 @@ class LyngEnterHandler : EnterHandlerDelegate { } // Adjust indent for the current (new) line val currentStart = doc.getLineStartOffsetSafe(currentLine) - val csm = CodeStyleManager.getInstance(project) - csm.adjustLineIndent(file, currentStart) + if (file.context == null) { + try { + CodeStyleManager.getInstance(project).adjustLineIndent(file, currentStart) + } catch (e: Exception) { + log.warn("Failed to adjust line indent for current line: ${e.message}") + } + } // Fallback: if the platform didn't physically insert indentation, compute it from our formatter and apply val lineStart = doc.getLineStartOffset(currentLine) diff --git a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/editor/LyngTypedHandler.kt b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/editor/LyngTypedHandler.kt index a22e9c4..a260b06 100644 --- a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/editor/LyngTypedHandler.kt +++ b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/editor/LyngTypedHandler.kt @@ -58,7 +58,13 @@ class LyngTypedHandler : TypedHandlerDelegate() { } // After block reindent, adjust line indent to what platform thinks (no-op in many cases) val lineStart = doc.getLineStartOffset(line) - CodeStyleManager.getInstance(project).adjustLineIndent(file, lineStart) + if (file.context == null) { + try { + CodeStyleManager.getInstance(project).adjustLineIndent(file, lineStart) + } catch (e: Exception) { + log.warn("Failed to adjust line indent for current line: ${e.message}") + } + } } } else if (c == '/') { val doc = editor.document @@ -67,7 +73,13 @@ class LyngTypedHandler : TypedHandlerDelegate() { PsiDocumentManager.getInstance(project).commitDocument(doc) val line = doc.getLineNumber(offset - 1) val lineStart = doc.getLineStartOffset(line) - CodeStyleManager.getInstance(project).adjustLineIndent(file, lineStart) + if (file.context == null) { + try { + CodeStyleManager.getInstance(project).adjustLineIndent(file, lineStart) + } catch (e: Exception) { + log.warn("Failed to adjust line indent for comment: ${e.message}") + } + } // Manual application fallback val desired = computeDesiredIndent(project, doc, line) diff --git a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/format/LyngPreFormatProcessor.kt b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/format/LyngPreFormatProcessor.kt index fd2f01a..81f0121 100644 --- a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/format/LyngPreFormatProcessor.kt +++ b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/format/LyngPreFormatProcessor.kt @@ -1,5 +1,5 @@ /* - * 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"); * you may not use this file except in compliance with the License. @@ -80,7 +80,14 @@ class LyngPreFormatProcessor : PreFormatProcessor { val lineStart = doc.getLineStartOffset(line) // adjustLineIndent delegates to our LineIndentProvider which computes // indentation from scratch; this is safe and idempotent - CodeStyleManager.getInstance(project).adjustLineIndent(file, lineStart) + if (file.context == null) { + try { + CodeStyleManager.getInstance(project).adjustLineIndent(file, lineStart) + } catch (e: Exception) { + // Log as debug because this can be called many times during reformat + // and we don't want to spam warnings if it's a known platform issue with injections + } + } // After indentation, update block/paren/bracket balances using the current line text val lineEnd = doc.getLineEndOffset(line) diff --git a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/navigation/LyngFindUsagesProvider.kt b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/navigation/LyngFindUsagesProvider.kt index db9a149..f042edb 100644 --- a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/navigation/LyngFindUsagesProvider.kt +++ b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/navigation/LyngFindUsagesProvider.kt @@ -17,7 +17,6 @@ package net.sergeych.lyng.idea.navigation -import LyngAstManager import com.intellij.lang.cacheBuilder.DefaultWordsScanner import com.intellij.lang.cacheBuilder.WordsScanner import com.intellij.lang.findUsages.FindUsagesProvider @@ -26,6 +25,7 @@ import com.intellij.psi.PsiElement import com.intellij.psi.tree.TokenSet import net.sergeych.lyng.idea.highlight.LyngLexer import net.sergeych.lyng.idea.highlight.LyngTokenTypes +import net.sergeych.lyng.idea.util.LyngAstManager import net.sergeych.lyng.miniast.DocLookupUtils class LyngFindUsagesProvider : FindUsagesProvider { diff --git a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/navigation/LyngIconProvider.kt b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/navigation/LyngIconProvider.kt index 3aac7e2..4ce582f 100644 --- a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/navigation/LyngIconProvider.kt +++ b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/navigation/LyngIconProvider.kt @@ -17,10 +17,10 @@ package net.sergeych.lyng.idea.navigation -import LyngAstManager import com.intellij.icons.AllIcons import com.intellij.ide.IconProvider import com.intellij.psi.PsiElement +import net.sergeych.lyng.idea.util.LyngAstManager import net.sergeych.lyng.miniast.DocLookupUtils import javax.swing.Icon diff --git a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/navigation/LyngPsiReference.kt b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/navigation/LyngPsiReference.kt index 55660c3..2cc1114 100644 --- a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/navigation/LyngPsiReference.kt +++ b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/navigation/LyngPsiReference.kt @@ -17,13 +17,13 @@ package net.sergeych.lyng.idea.navigation -import LyngAstManager import com.intellij.openapi.project.Project import com.intellij.openapi.util.TextRange import com.intellij.psi.* import com.intellij.psi.search.FilenameIndex import com.intellij.psi.search.GlobalSearchScope import net.sergeych.lyng.highlight.offsetOf +import net.sergeych.lyng.idea.util.LyngAstManager import net.sergeych.lyng.idea.util.TextCtx import net.sergeych.lyng.miniast.* diff --git a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/navigation/LyngPsiReferenceContributor.kt b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/navigation/LyngPsiReferenceContributor.kt index c857590..56b5a6f 100644 --- a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/navigation/LyngPsiReferenceContributor.kt +++ b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/navigation/LyngPsiReferenceContributor.kt @@ -17,12 +17,12 @@ package net.sergeych.lyng.idea.navigation -import LyngAstManager import com.intellij.patterns.PlatformPatterns import com.intellij.psi.* import com.intellij.util.ProcessingContext import net.sergeych.lyng.idea.LyngLanguage import net.sergeych.lyng.idea.highlight.LyngTokenTypes +import net.sergeych.lyng.idea.util.LyngAstManager import net.sergeych.lyng.miniast.DocLookupUtils class LyngPsiReferenceContributor : PsiReferenceContributor() { diff --git a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/util/LyngAstManager.kt b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/util/LyngAstManager.kt index 496ce18..8df0e3c 100644 --- a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/util/LyngAstManager.kt +++ b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/util/LyngAstManager.kt @@ -15,23 +15,6 @@ * */ -a/* - * 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. - * - */ - package net.sergeych.lyng.idea.util import com.intellij.openapi.util.Key diff --git a/lynglib/build.gradle.kts b/lynglib/build.gradle.kts index e755f26..7c4d8fd 100644 --- a/lynglib/build.gradle.kts +++ b/lynglib/build.gradle.kts @@ -1,5 +1,5 @@ /* - * 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"); * you may not use this file except in compliance with the License. @@ -21,7 +21,7 @@ import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl import org.jetbrains.kotlin.gradle.dsl.JvmTarget group = "net.sergeych" -version = "1.1.0-SNAPSHOT" +version = "1.1.0-rc" // Removed legacy buildscript classpath declarations; plugins are applied via the plugins DSL below diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/format/LyngFormatter.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/format/LyngFormatter.kt index ad465c8..bcb9b31 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/format/LyngFormatter.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/format/LyngFormatter.kt @@ -83,7 +83,7 @@ object LyngFormatter { // property accessors ending with ) or = if (isPropertyAccessor(t)) { - return t.endsWith(")") || t.endsWith("=") + return if (t.contains('=')) t.endsWith('=') else t.endsWith(')') } return false } @@ -186,7 +186,7 @@ object LyngFormatter { val endsWithBrace = code.trimEnd().endsWith("{") if (!endsWithBrace && isControlHeaderNoBrace(code)) { // It's another header, increment - awaitingExtraIndent += 1 + awaitingExtraIndent += if (isAccessor) 2 else 1 } else { // It's the body, reset awaitingExtraIndent = 0 @@ -195,7 +195,7 @@ object LyngFormatter { // start awaiting if current line is a control header without '{' val endsWithBrace = code.trimEnd().endsWith("{") if (!endsWithBrace && isControlHeaderNoBrace(code)) { - awaitingExtraIndent = 1 + awaitingExtraIndent = if (isAccessor) 2 else 1 } } diff --git a/lynglib/src/commonTest/kotlin/net/sergeych/lyng/format/LyngFormatterTest.kt b/lynglib/src/commonTest/kotlin/net/sergeych/lyng/format/LyngFormatterTest.kt index 10e3fcf..859c911 100644 --- a/lynglib/src/commonTest/kotlin/net/sergeych/lyng/format/LyngFormatterTest.kt +++ b/lynglib/src/commonTest/kotlin/net/sergeych/lyng/format/LyngFormatterTest.kt @@ -802,4 +802,52 @@ class LyngFormatterTest { val out = LyngFormatter.reindent(src, cfg) assertEquals(expected, out) } + + @Test + fun propertyAccessor_followedByMethod() { + val src = """ + var state: BarRequestState get() = getState() + set(value) = setState(value) + + fun save() { cell.value = this } + """.trimIndent() + + val expected = """ + var state: BarRequestState get() = getState() + set(value) = setState(value) + + fun save() { cell.value = this } + """.trimIndent() + + val cfg = LyngFormatConfig(indentSize = 4, continuationIndentSize = 4) + val out = LyngFormatter.reindent(src, cfg) + assertEquals(expected, out) + } + + @Test + fun propertyAccessor_dangling() { + val src1 = """ + var x + get() + = 1 + """.trimIndent() + val expected1 = """ + var x + get() + = 1 + """.trimIndent() + assertEquals(expected1, LyngFormatter.reindent(src1, LyngFormatConfig(indentSize = 4))) + + val src2 = """ + var x + get() = + 1 + """.trimIndent() + val expected2 = """ + var x + get() = + 1 + """.trimIndent() + assertEquals(expected2, LyngFormatter.reindent(src2, LyngFormatConfig(indentSize = 4))) + } } diff --git a/site/src/jsMain/kotlin/ReferencePage.kt b/site/src/jsMain/kotlin/ReferencePage.kt index f893982..264157f 100644 --- a/site/src/jsMain/kotlin/ReferencePage.kt +++ b/site/src/jsMain/kotlin/ReferencePage.kt @@ -1,5 +1,5 @@ /* - * 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"); * you may not use this file except in compliance with the License. @@ -127,7 +127,7 @@ fun ReferencePage() { Ul({ classes("mt-2") }) { d.members.forEach { m -> when (m) { - is MiniMemberFunDecl, -> { + is MiniMemberFunDecl -> { val params = m.params.joinToString(", ") { p -> val ts = typeOf(p.type) if (ts.isNotBlank()) "${p.name}${ts}" else p.name @@ -150,6 +150,17 @@ fun ReferencePage() { } } } + is MiniEnumDecl -> { + Div { Text("enum ${d.name}") } + d.doc?.summary?.let { Small({ classes("text-muted") }) { Text(it) } } + if (d.entries.isNotEmpty()) { + Ul({ classes("mt-2") }) { + d.entries.forEach { entry -> + Li { Text(entry) } + } + } + } + } } } }