From 5dc2159024a2cc1aa3c7cd9c40e07a4175a628ce Mon Sep 17 00:00:00 2001 From: sergeych Date: Sun, 18 Jan 2026 07:18:51 +0300 Subject: [PATCH] added clamp function and extensions, fixed bug in range coloring --- docs/Range.md | 7 ++- docs/Real.md | 1 + docs/math.md | 6 ++ docs/whats_new.md | 20 +++++++ .../idea/docs/LyngDocumentationProvider.kt | 18 +++--- .../sergeych/lyng/idea/highlight/LyngLexer.kt | 60 ++++++++++++++++++- .../kotlin/net/sergeych/lyng/Script.kt | 41 +++++++++++-- .../kotlin/net/sergeych/lyng/obj/Obj.kt | 32 ++++++++++ lynglib/src/commonTest/kotlin/ScriptTest.kt | 36 +++++++++++ 9 files changed, 205 insertions(+), 16 deletions(-) diff --git a/docs/Range.md b/docs/Range.md index cf92d20..909d0e3 100644 --- a/docs/Range.md +++ b/docs/Range.md @@ -98,10 +98,11 @@ Exclusive end char ranges are supported too: | isEndInclusive | true for '..' | Bool | | isOpen | at any end | Bool | | isIntRange | both start and end are Int | Bool | -| start | | Bool | -| end | | Bool | +| start | | Any? | +| end | | Any? | | size | for finite ranges, see above | Long | | [] | see above | | -| | | | + +Ranges are also used with the `clamp(value, range)` function and the `value.clamp(range)` extension method to limit values within boundaries. [Iterable]: Iterable.md \ No newline at end of file diff --git a/docs/Real.md b/docs/Real.md index 2f63c7a..0e9b484 100644 --- a/docs/Real.md +++ b/docs/Real.md @@ -19,6 +19,7 @@ you can use it's class to ensure type: |-----------------|-------------------------------------------------------------|------| | `.roundToInt()` | round to nearest int like round(x) | Int | | `.toInt()` | convert integer part of real to `Int` dropping decimal part | Int | +| `.clamp(range)` | clamp value within range boundaries | Real | | | | | | | | | | | | | diff --git a/docs/math.md b/docs/math.md index 743d54a..d8a67a9 100644 --- a/docs/math.md +++ b/docs/math.md @@ -92,6 +92,7 @@ or transformed `Real` otherwise. | pow(x, y) | ${x^y}$ | | sqrt(x) | $ \sqrt {x}$ | | abs(x) | absolute value of x. Int if x is Int, Real otherwise | +| clamp(x, range) | limit x to be inside range boundaries | For example: @@ -102,6 +103,11 @@ For example: // abs() keeps the argument type: assert( abs(-1) is Int) assert( abs(-2.21) == 2.21 ) + + // clamp() limits value to the range: + assert( clamp(15, 0..10) == 10 ) + assert( clamp(-5, 0..10) == 0 ) + assert( 5.clamp(0..10) == 5 ) >>> void ## Scientific constant diff --git a/docs/whats_new.md b/docs/whats_new.md index e8ecf24..59bcddb 100644 --- a/docs/whats_new.md +++ b/docs/whats_new.md @@ -186,6 +186,26 @@ Key features: - **Structural Equality**: Transient fields are automatically ignored during `==` equality checks. - **Deserialization**: Transient constructor parameters with default values are correctly restored to those defaults upon restoration. +### Value Clamping (`clamp`) +A new `clamp()` function has been added to the standard library to limit a value within a specified range. It is available as both a global function and an extension method on all objects. + +```lyng +// Global function +clamp(15, 0..10) // returns 10 +clamp(-5, 0..10) // returns 0 + +// Extension method +val x = 15 +x.clamp(0..10) // returns 10 + +// Exclusive and open-ended ranges +15.clamp(0..<10) // returns 9 +15.clamp(..10) // returns 10 +-5.clamp(0..) // returns 0 +``` + +`clamp()` correctly handles inclusive (`..`) and exclusive (`..<`) ranges. For discrete types like `Int` and `Char`, clamping to an exclusive upper bound returns the previous value. + ## Tooling and Infrastructure ### CLI: Formatting Command diff --git a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/docs/LyngDocumentationProvider.kt b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/docs/LyngDocumentationProvider.kt index abe7a72..82e4cef 100644 --- a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/docs/LyngDocumentationProvider.kt +++ b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/docs/LyngDocumentationProvider.kt @@ -139,7 +139,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() { val e: Int = miniSource.offsetOf(d.range.end) if (offset >= s && offset <= e) { // For enum constant, we don't have detailed docs in MiniAst yet, but we can render a title - return "
enum constant ${d.name}.${name}
" + return renderTitle("enum constant ${d.name}.${name}") } } } @@ -361,7 +361,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() { "Print values to the standard output and append a newline. Accepts any number of arguments." else "Print values to the standard output without a trailing newline. Accepts any number of arguments." val title = "function $ident(values)" - return "
${htmlEscape(title)}
" + styledMarkdown(htmlEscape(fallback)) + return renderTitle(title) + styledMarkdown(htmlEscape(fallback)) } // 4b) try class members like ClassName.member with inheritance fallback val lhs = previousWordBefore(text, idRange.startOffset) @@ -519,7 +519,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() { } } val sb = StringBuilder() - sb.append("
").append(htmlEscape(title)).append("
") + sb.append(renderTitle(title)) sb.append(renderDocBody(d.doc)) return sb.toString() } @@ -527,7 +527,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() { private fun renderParamDoc(fn: MiniFunDecl, p: MiniParam): String { val title = "parameter ${p.name}${typeOf(p.type)} in ${fn.name}${signatureOf(fn)}" val sb = StringBuilder() - sb.append("
").append(htmlEscape(title)).append("
") + sb.append(renderTitle(title)) // Find matching @param tag fn.doc?.tags?.get("param")?.forEach { tag -> @@ -549,7 +549,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() { val staticStr = if (m.isStatic) "static " else "" val title = "${staticStr}method $className.${m.name}(${params})${ret}" val sb = StringBuilder() - sb.append("
").append(htmlEscape(title)).append("
") + sb.append(renderTitle(title)) sb.append(renderDocBody(m.doc)) return sb.toString() } @@ -560,7 +560,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() { val staticStr = if (m.isStatic) "static " else "" val title = "${staticStr}${kind} $className.${m.name}${ts}" val sb = StringBuilder() - sb.append("
").append(htmlEscape(title)).append("
") + sb.append(renderTitle(title)) sb.append(renderDocBody(m.doc)) return sb.toString() } @@ -579,6 +579,10 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() { return "(${params})${ret}" } + private fun renderTitle(title: String): String { + return "
${htmlEscape(title)}
" + } + private fun htmlEscape(s: String): String = buildString(s.length) { for (ch in s) append( when (ch) { @@ -645,7 +649,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() { private fun renderOverloads(name: String, overloads: List): String { val sb = StringBuilder() - sb.append("
Overloads for ").append(htmlEscape(name)).append("
") + sb.append(renderTitle("Overloads for $name")) sb.append("