fixed fromatter, plugin, site, libs for new language features

This commit is contained in:
Sergey Chernov 2026-01-04 01:25:16 +01:00
parent abb262d9cf
commit 1d9befe101
14 changed files with 218 additions and 36 deletions

110
docs/whats_new.md Normal file
View File

@ -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**.

View File

@ -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.*

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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 {

View File

@ -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

View File

@ -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.*

View File

@ -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() {

View File

@ -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

View File

@ -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

View File

@ -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
}
}

View File

@ -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)))
}
}

View File

@ -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) }
}
}
}
}
}
}
}