fixed fromatter, plugin, site, libs for new language features
This commit is contained in:
parent
abb262d9cf
commit
1d9befe101
110
docs/whats_new.md
Normal file
110
docs/whats_new.md
Normal 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**.
|
||||
@ -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.*
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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.*
|
||||
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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)))
|
||||
}
|
||||
}
|
||||
|
||||
@ -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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user