Added delegation support: Delegate, lazy, "by" keyword for valr/var/fun and object {} singletons
This commit is contained in:
1 parent
8e766490d9
commit
5f819dc87a
20 files changed
+1183
-754
No files matched your search
@@ -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.
|
||||
@@ -318,6 +318,21 @@ fun EditorWithOverlay(
|
||||
try { ta.setSelectionRange(res.selStart, res.selEnd) } catch (_: Throwable) {}
|
||||
pendingSelStart = res.selStart
|
||||
pendingSelEnd = res.selEnd
|
||||
} else if (key.length == 1 && !ev.ctrlKey && !ev.metaKey && !ev.altKey) {
|
||||
// Handle single character input (like '}') for dedenting
|
||||
// This is an alternative to onInput to have better control
|
||||
val start = ta.selectionStart ?: 0
|
||||
val end = ta.selectionEnd ?: start
|
||||
val current = ta.value
|
||||
val res = applyChar(current, start, end, key[0], tabSize)
|
||||
if (res.text != (current.substring(0, start) + key + current.substring(end))) {
|
||||
// Logic decided to change something else (e.g. dedent)
|
||||
ev.preventDefault()
|
||||
setCode(res.text)
|
||||
try { ta.setSelectionRange(res.selStart, res.selEnd) } catch (_: Throwable) {}
|
||||
pendingSelStart = res.selStart
|
||||
pendingSelEnd = res.selEnd
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -83,55 +83,6 @@ private fun nextNonWs(text: String, idxInclusive: Int): Int {
|
||||
|
||||
/** Apply Enter key behavior with smart indent/undent rules. */
|
||||
fun applyEnter(text: String, selStart: Int, selEnd: Int, tabSize: Int): EditResult {
|
||||
// Global early rule (Rule 3): if the line after the current line is brace-only, dedent that line by one block and
|
||||
// do NOT insert a newline. This uses precise line boundaries.
|
||||
run {
|
||||
val start = minOf(selStart, text.length)
|
||||
val eol = lineEndAt(text, start)
|
||||
val nextLineStart = if (eol < text.length && text[eol] == '\n') eol + 1 else eol
|
||||
val nextLineEnd = lineEndAt(text, nextLineStart)
|
||||
if (nextLineStart <= nextLineEnd && nextLineStart < text.length) {
|
||||
val trimmedNext = text.substring(nextLineStart, nextLineEnd).trim()
|
||||
if (trimmedNext == "}") {
|
||||
val rbraceIndent = countIndentSpaces(text, nextLineStart, nextLineEnd)
|
||||
val removeCount = kotlin.math.min(tabSize, rbraceIndent)
|
||||
val crShift = if (nextLineStart < text.length && text[nextLineStart] == '\r') 1 else 0
|
||||
val out = buildString(text.length) {
|
||||
append(safeSubstring(text, 0, nextLineStart))
|
||||
append(safeSubstring(text, nextLineStart + crShift + removeCount, text.length))
|
||||
}
|
||||
val caret = nextLineStart + kotlin.math.max(0, rbraceIndent - removeCount)
|
||||
return EditResult(out, caret, caret)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Absolute top-priority: caret is exactly at a line break and the next line is '}'-only (ignoring spaces).
|
||||
// Dedent that next line by one block (tabSize) and do NOT insert a newline.
|
||||
run {
|
||||
if (selStart < text.length) {
|
||||
val isCrLf = selStart + 1 < text.length && text[selStart] == '\r' && text[selStart + 1] == '\n'
|
||||
val isLf = text[selStart] == '\n'
|
||||
if (isCrLf || isLf) {
|
||||
val nextLineStart = selStart + if (isCrLf) 2 else 1
|
||||
val nextLineEnd = lineEndAt(text, nextLineStart)
|
||||
if (nextLineStart <= nextLineEnd) {
|
||||
val trimmed = text.substring(nextLineStart, nextLineEnd).trim()
|
||||
if (trimmed == "}") {
|
||||
val rbraceIndent = countIndentSpaces(text, nextLineStart, nextLineEnd)
|
||||
val removeCount = kotlin.math.min(tabSize, rbraceIndent)
|
||||
val crShift = if (nextLineStart < text.length && text[nextLineStart] == '\r') 1 else 0
|
||||
val out = buildString(text.length) {
|
||||
append(safeSubstring(text, 0, nextLineStart))
|
||||
append(safeSubstring(text, nextLineStart + crShift + removeCount, text.length))
|
||||
}
|
||||
val caret = nextLineStart + kotlin.math.max(0, rbraceIndent - removeCount)
|
||||
return EditResult(out, caret, caret)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If there is a selection, replace it by newline + current line indent
|
||||
if (selEnd != selStart) {
|
||||
val lineStart = lineStartAt(text, selStart)
|
||||
@@ -148,8 +99,6 @@ fun applyEnter(text: String, selStart: Int, selEnd: Int, tabSize: Int): EditResu
|
||||
val lineEnd = lineEndAt(text, start)
|
||||
val indent = countIndentSpaces(text, lineStart, lineEnd)
|
||||
|
||||
// (Handled by the global early rule above; no need for additional EOL variants.)
|
||||
|
||||
// Compute neighborhood characters early so rule precedence can use them
|
||||
val prevIdx = prevNonWs(text, start)
|
||||
val nextIdx = nextNonWs(text, start)
|
||||
@@ -158,85 +107,6 @@ fun applyEnter(text: String, selStart: Int, selEnd: Int, tabSize: Int): EditResu
|
||||
val before = text.substring(0, start)
|
||||
val after = text.substring(start)
|
||||
|
||||
// Rule 2: On a brace-only line '}' (caret on the same line)
|
||||
// If the current line’s trimmed text is exactly '}', decrease that line’s indent by one block (not below 0),
|
||||
// then insert a newline. The newly inserted line uses the (decreased) indent. Place the caret at the start of
|
||||
// the newly inserted line.
|
||||
run {
|
||||
val trimmed = text.substring(lineStart, lineEnd).trim()
|
||||
// IMPORTANT: Rule precedence — do NOT trigger this rule when caret is after '}' and the rest of the line
|
||||
// up to EOL contains only spaces (Rule 5 handles that case). That scenario must insert AFTER the brace,
|
||||
// not before it.
|
||||
var onlySpacesAfterCaret = true
|
||||
var k = start
|
||||
while (k < lineEnd) { if (text[k] != ' ') { onlySpacesAfterCaret = false; break }; k++ }
|
||||
val rule5Situation = (prevCh == '}') && onlySpacesAfterCaret
|
||||
|
||||
if (trimmed == "}" && !rule5Situation) {
|
||||
val removeCount = kotlin.math.min(tabSize, indent)
|
||||
val newIndent = (indent - removeCount).coerceAtLeast(0)
|
||||
val crShift = if (lineStart < text.length && text[lineStart] == '\r') 1 else 0
|
||||
val out = buildString(text.length + 1 + newIndent) {
|
||||
append(safeSubstring(text, 0, lineStart))
|
||||
append("\n")
|
||||
append(" ".repeat(newIndent))
|
||||
// Write the brace line but with its indent reduced by removeCount spaces
|
||||
append(safeSubstring(text, lineStart + crShift + removeCount, text.length))
|
||||
}
|
||||
val caret = lineStart + 1 + newIndent
|
||||
return EditResult(out, caret, caret)
|
||||
}
|
||||
}
|
||||
|
||||
// (The special case of caret after the last non-ws on a '}'-only line is covered by the rule above.)
|
||||
|
||||
// 0) Caret is at end-of-line and the next line is a closing brace-only line: dedent that line, no extra newline
|
||||
run {
|
||||
val atCr = start + 1 < text.length && text[start] == '\r' && text[start + 1] == '\n'
|
||||
val atNl = start < text.length && text[start] == '\n'
|
||||
val atEol = atNl || atCr
|
||||
if (atEol) {
|
||||
val nlAdvance = if (atCr) 2 else 1
|
||||
val nextLineStart = start + nlAdvance
|
||||
val nextLineEnd = lineEndAt(text, nextLineStart)
|
||||
val trimmedNext = text.substring(nextLineStart, nextLineEnd).trim()
|
||||
if (trimmedNext == "}") {
|
||||
val rbraceIndent = countIndentSpaces(text, nextLineStart, nextLineEnd)
|
||||
// Dedent the '}' line by one block level (tabSize), but not below column 0
|
||||
val removeCount = kotlin.math.min(tabSize, rbraceIndent)
|
||||
val crShift = if (nextLineStart < text.length && text[nextLineStart] == '\r') 1 else 0
|
||||
val out = buildString(text.length) {
|
||||
append(safeSubstring(text, 0, nextLineStart))
|
||||
append(safeSubstring(text, nextLineStart + crShift + removeCount, text.length))
|
||||
}
|
||||
val caret = start + nlAdvance + kotlin.math.max(0, rbraceIndent - removeCount)
|
||||
return EditResult(out, caret, caret)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 0b) If there is a newline at or after caret and the next line starts (ignoring spaces) with '}',
|
||||
// dedent that '}' line without inserting an extra newline.
|
||||
run {
|
||||
val nlPos = text.indexOf('\n', start)
|
||||
if (nlPos >= 0) {
|
||||
val nextLineStart = nlPos + 1
|
||||
val nextLineEnd = lineEndAt(text, nextLineStart)
|
||||
val nextLineFirstNonWs = nextNonWs(text, nextLineStart)
|
||||
if (nextLineFirstNonWs in nextLineStart until nextLineEnd && text[nextLineFirstNonWs] == '}') {
|
||||
val rbraceIndent = countIndentSpaces(text, nextLineStart, nextLineEnd)
|
||||
val removeCount = kotlin.math.min(tabSize, rbraceIndent)
|
||||
val crShift = if (nextLineStart < text.length && text[nextLineStart] == '\r') 1 else 0
|
||||
val out = buildString(text.length) {
|
||||
append(safeSubstring(text, 0, nextLineStart))
|
||||
append(safeSubstring(text, nextLineStart + crShift + removeCount, text.length))
|
||||
}
|
||||
val caret = nextLineStart + kotlin.math.max(0, rbraceIndent - removeCount)
|
||||
return EditResult(out, caret, caret)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 1) Between braces { | } -> two lines, inner indented
|
||||
if (prevCh == '{' && nextCh == '}') {
|
||||
val innerIndent = indent + tabSize
|
||||
@@ -254,119 +124,17 @@ fun applyEnter(text: String, selStart: Int, selEnd: Int, tabSize: Int): EditResu
|
||||
}
|
||||
// 3) Before '}'
|
||||
if (nextCh == '}') {
|
||||
// We want two things:
|
||||
// - reduce indentation of the upcoming '}' line by one level
|
||||
// - avoid creating an extra blank line if caret is already at EOL (the next char is a newline)
|
||||
|
||||
// Compute where the '}' line starts and how many leading spaces it has
|
||||
val rbraceLineStart = lineStartAt(text, nextIdx)
|
||||
val rbraceLineEnd = lineEndAt(text, nextIdx)
|
||||
val rbraceIndent = countIndentSpaces(text, rbraceLineStart, rbraceLineEnd)
|
||||
// Dedent the '}' line by one block level (tabSize), but not below column 0
|
||||
val removeCount = kotlin.math.min(tabSize, rbraceIndent)
|
||||
val crShift = if (rbraceLineStart < text.length && text[rbraceLineStart] == '\r') 1 else 0
|
||||
|
||||
// If there is already a newline between caret and the '}', do NOT insert another newline.
|
||||
// Just dedent the existing '}' line by one block and place caret at its start.
|
||||
run {
|
||||
val nlBetween = text.indexOf('\n', start)
|
||||
if (nlBetween in start until rbraceLineStart) {
|
||||
val out = buildString(text.length) {
|
||||
append(safeSubstring(text, 0, rbraceLineStart))
|
||||
append(safeSubstring(text, rbraceLineStart + crShift + removeCount, text.length))
|
||||
}
|
||||
val caret = rbraceLineStart + kotlin.math.max(0, rbraceIndent - removeCount)
|
||||
return EditResult(out, caret, caret)
|
||||
}
|
||||
}
|
||||
|
||||
val hasNewlineAtCaret = (start < text.length && text[start] == '\n')
|
||||
|
||||
// New indentation for the line we create (if we actually insert one now)
|
||||
val newLineIndent = (indent - tabSize).coerceAtLeast(0)
|
||||
val insertion = if (hasNewlineAtCaret) "" else "\n" + " ".repeat(newLineIndent)
|
||||
|
||||
val out = buildString(text.length + insertion.length) {
|
||||
append(before)
|
||||
append(insertion)
|
||||
// keep text up to the start of '}' line
|
||||
append(safeSubstring(text, start, rbraceLineStart))
|
||||
// drop up to tabSize spaces before '}'
|
||||
append(safeSubstring(text, rbraceLineStart + crShift + removeCount, text.length))
|
||||
}
|
||||
val caret = if (hasNewlineAtCaret) {
|
||||
// Caret moves to the beginning of the '}' line after dedent (right after the single newline)
|
||||
start + 1 + kotlin.math.max(0, rbraceIndent - removeCount)
|
||||
} else {
|
||||
start + insertion.length
|
||||
}
|
||||
return EditResult(out, caret, caret)
|
||||
}
|
||||
// 4) After '}' with only trailing spaces before EOL
|
||||
// According to Rule 5: if the last non-whitespace before the caret is '}' and
|
||||
// only spaces remain until EOL, we must:
|
||||
// - dedent the current (brace) line by one block (not below 0)
|
||||
// - insert a newline just AFTER '}' (do NOT move caret backward)
|
||||
// - set the caret at the start of the newly inserted blank line, whose indent equals the dedented indent
|
||||
if (prevCh == '}') {
|
||||
var onlySpaces = true
|
||||
var k = prevIdx + 1
|
||||
while (k < lineEnd) { if (text[k] != ' ') { onlySpaces = false; break }; k++ }
|
||||
if (onlySpaces) {
|
||||
val removeCount = kotlin.math.min(tabSize, indent)
|
||||
val newIndent = (indent - removeCount).coerceAtLeast(0)
|
||||
val crShift = if (lineStart < text.length && text[lineStart] == '\r') 1 else 0
|
||||
|
||||
// Build the result:
|
||||
// - keep everything before the line start
|
||||
// - write the current line content up to the caret, but with its left indent reduced by removeCount
|
||||
// - insert newline + spaces(newIndent)
|
||||
// - drop trailing spaces after caret up to EOL
|
||||
// - keep the rest of the text starting from EOL
|
||||
val out = buildString(text.length) {
|
||||
append(safeSubstring(text, 0, lineStart))
|
||||
append(safeSubstring(text, lineStart + crShift + removeCount, start))
|
||||
append("\n")
|
||||
append(" ".repeat(newIndent))
|
||||
append(safeSubstring(text, lineEnd, text.length))
|
||||
}
|
||||
val caret = (lineStart + (start - (lineStart + crShift + removeCount)) + 1 + newIndent)
|
||||
return EditResult(out, caret, caret)
|
||||
} else {
|
||||
// Default smart indent for cases where there are non-space characters after '}'
|
||||
val insertion = "\n" + " ".repeat(indent)
|
||||
val out = before + insertion + after
|
||||
val caret = start + insertion.length
|
||||
return EditResult(out, caret, caret)
|
||||
}
|
||||
}
|
||||
// 5) Fallback: if there is a newline ahead and the next line, trimmed, equals '}', dedent that '}' line by one block
|
||||
run {
|
||||
val nlPos = text.indexOf('\n', start)
|
||||
if (nlPos >= 0) {
|
||||
val nextLineStart = nlPos + 1
|
||||
val nextLineEnd = lineEndAt(text, nextLineStart)
|
||||
val trimmedNext = text.substring(nextLineStart, nextLineEnd).trim()
|
||||
if (trimmedNext == "}") {
|
||||
val rbraceIndent = countIndentSpaces(text, nextLineStart, nextLineEnd)
|
||||
val removeCount = kotlin.math.min(tabSize, rbraceIndent)
|
||||
val crShift = if (nextLineStart < text.length && text[nextLineStart] == '\r') 1 else 0
|
||||
val out = buildString(text.length) {
|
||||
append(safeSubstring(text, 0, nextLineStart))
|
||||
append(safeSubstring(text, nextLineStart + crShift + removeCount, text.length))
|
||||
}
|
||||
val caret = nextLineStart + kotlin.math.max(0, rbraceIndent - removeCount)
|
||||
return EditResult(out, caret, caret)
|
||||
}
|
||||
}
|
||||
}
|
||||
// default keep same indent
|
||||
run {
|
||||
val insertion = "\n" + " ".repeat(indent)
|
||||
val out = before + insertion + after
|
||||
val caret = start + insertion.length
|
||||
return EditResult(out, caret, caret)
|
||||
}
|
||||
|
||||
// default keep same indent
|
||||
val insertion = "\n" + " ".repeat(indent)
|
||||
val out = before + insertion + after
|
||||
val caret = start + insertion.length
|
||||
return EditResult(out, caret, caret)
|
||||
}
|
||||
|
||||
/** Apply Tab key: insert spaces at caret (single-caret only). */
|
||||
@@ -423,3 +191,37 @@ fun applyShiftTab(text: String, selStart: Int, selEnd: Int, tabSize: Int): EditR
|
||||
val e = maxOf(newSelStart, newSelEnd)
|
||||
return EditResult(sb.toString(), s, e)
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a typed character. If the character is '}', and it's the only non-whitespace on the line,
|
||||
* it may be dedented.
|
||||
*/
|
||||
fun applyChar(text: String, selStart: Int, selEnd: Int, ch: Char, tabSize: Int): EditResult {
|
||||
// Selection replacement
|
||||
val current = if (selStart != selEnd) {
|
||||
text.substring(0, minOf(selStart, selEnd)) + text.substring(maxOf(selStart, selEnd))
|
||||
} else text
|
||||
val pos = minOf(selStart, selEnd)
|
||||
|
||||
val before = current.substring(0, pos)
|
||||
val after = current.substring(pos)
|
||||
val newText = before + ch + after
|
||||
val newPos = pos + 1
|
||||
|
||||
if (ch == '}') {
|
||||
val lineStart = lineStartAt(newText, pos)
|
||||
val lineEnd = lineEndAt(newText, newPos)
|
||||
val trimmed = newText.substring(lineStart, lineEnd).trim()
|
||||
if (trimmed == "}") {
|
||||
// Dedent this line
|
||||
val indent = countIndentSpaces(newText, lineStart, lineEnd)
|
||||
val removeCount = minOf(tabSize, indent)
|
||||
if (removeCount > 0) {
|
||||
val out = newText.substring(0, lineStart) + newText.substring(lineStart + removeCount)
|
||||
return EditResult(out, newPos - removeCount, newPos - removeCount)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return EditResult(newText, newPos, newPos)
|
||||
}
|
||||
@@ -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.
|
||||
@@ -25,81 +25,19 @@ class EditorLogicTest {
|
||||
private val tab = 4
|
||||
|
||||
@Test
|
||||
fun enter_after_only_rbrace_undents() {
|
||||
val line = " } " // 4 spaces, brace, trailing spaces; caret after last non-ws
|
||||
val res = applyEnter(line, line.length, line.length, tab)
|
||||
// Should insert newline with one indent level less (0 spaces here)
|
||||
assertEquals(" } \n" + "" , res.text.substring(0, res.text.indexOf('\n')+1))
|
||||
// After insertion caret should be at end of inserted indentation
|
||||
// Here indent was 4 and undented by 4 -> 0 spaces after newline
|
||||
val expectedCaret = line.length + 1 + 0
|
||||
assertEquals(expectedCaret, res.selStart)
|
||||
assertEquals(expectedCaret, res.selEnd)
|
||||
fun type_rbrace_dedents_if_only_char_on_line() {
|
||||
val text = " "
|
||||
val res = applyChar(text, 4, 4, '}', tab)
|
||||
assertEquals("}", res.text)
|
||||
assertEquals(1, res.selStart)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun enter_after_rbrace_with_only_spaces_to_eol_inserts_after_brace_and_dedents_and_undents_brace_line() {
|
||||
// Rule 5 exact check: last non-ws before caret is '}', remainder to EOL only spaces
|
||||
val indents = listOf(0, 4, 8)
|
||||
for (indent in indents) {
|
||||
val spaces = " ".repeat(indent)
|
||||
// Line ends with '}' followed by three spaces
|
||||
val before = (
|
||||
"""
|
||||
1
|
||||
${'$'}spaces}
|
||||
"""
|
||||
).trimIndent() + " "
|
||||
// Caret right after '}', before trailing spaces
|
||||
val caret = before.indexOf('}') + 1
|
||||
val res = applyEnter(before, caret, caret, tab)
|
||||
|
||||
val newBraceIndent = (indent - tab).coerceAtLeast(0)
|
||||
val newLineIndent = newBraceIndent
|
||||
val expected = buildString {
|
||||
append("1\n")
|
||||
append(" ".repeat(newBraceIndent))
|
||||
append("}\n")
|
||||
append(" ".repeat(newLineIndent))
|
||||
}
|
||||
assertEquals(expected, res.text)
|
||||
// Caret must be at start of the newly inserted line (after the final newline)
|
||||
assertEquals(expected.length, res.selStart)
|
||||
assertEquals(res.selStart, res.selEnd)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun enter_after_rbrace_with_only_spaces_to_eol_crlf_and_undents_brace_line() {
|
||||
val indent = 4
|
||||
val spaces = " ".repeat(indent)
|
||||
val beforeLf = (
|
||||
"""
|
||||
1
|
||||
${'$'}spaces}
|
||||
"""
|
||||
).trimIndent() + " "
|
||||
val before = beforeLf.replace("\n", "\r\n")
|
||||
val caret = before.indexOf('}') + 1
|
||||
val res = applyEnter(before, caret, caret, tab)
|
||||
val actual = res.text.replace("\r\n", "\n")
|
||||
val newIndent = (indent - tab).coerceAtLeast(0)
|
||||
val expected = "1\n${" ".repeat(newIndent)}}\n${" ".repeat(newIndent)}"
|
||||
assertEquals(expected, actual)
|
||||
assertEquals(expected.length, res.selStart)
|
||||
assertEquals(res.selStart, res.selEnd)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun enter_before_closing_brace_outdents() {
|
||||
val text = " }" // caret before '}' at index 4
|
||||
val caret = 4
|
||||
val res = applyEnter(text, caret, caret, tab)
|
||||
// Inserted a newline with indent reduced to 0
|
||||
assertEquals("\n" + "" + "}", res.text.substring(caret, caret + 2))
|
||||
val expectedCaret = caret + 1 + 0
|
||||
assertEquals(expectedCaret, res.selStart)
|
||||
assertEquals(expectedCaret, res.selEnd)
|
||||
fun type_rbrace_no_dedent_if_not_only_char() {
|
||||
val text = " foo "
|
||||
val res = applyChar(text, 8, 8, '}', tab)
|
||||
assertEquals(" foo }", res.text)
|
||||
assertEquals(9, res.selStart)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -123,23 +61,6 @@ class EditorLogicTest {
|
||||
assertEquals(1 + 1 + 4, res.selStart)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun enter_after_rbrace_line_undents_ignoring_trailing_ws() {
|
||||
// Line contains only '}' plus trailing spaces; caret after last non-ws
|
||||
val line = " } "
|
||||
val res = applyEnter(line, line.length, line.length, tab)
|
||||
// Expect insertion of a newline and an undented indentation of (indent - tab)
|
||||
val expectedIndentAfterNewline = 0 // 4 - 4
|
||||
val expected = line + "\n" + " ".repeat(expectedIndentAfterNewline)
|
||||
// Compare prefix up to inserted indentation
|
||||
val prefix = res.text.substring(0, expected.length)
|
||||
assertEquals(expected, prefix)
|
||||
// Caret positioned at end of inserted indentation
|
||||
val expectedCaret = expected.length
|
||||
assertEquals(expectedCaret, res.selStart)
|
||||
assertEquals(expectedCaret, res.selEnd)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun shift_tab_outdents_rbrace_only_line_no_newline() {
|
||||
// Multi-line: a block followed by a number; we outdent the '}' line only
|
||||
@@ -171,222 +92,55 @@ class EditorLogicTest {
|
||||
assertEquals(1 + 4, res.selStart)
|
||||
}
|
||||
|
||||
/*
|
||||
@Test
|
||||
fun enter_before_line_with_only_rbrace_dedents_that_line() {
|
||||
// Initial content as reported by user before fix
|
||||
val before = (
|
||||
"""
|
||||
{
|
||||
1
|
||||
2
|
||||
3
|
||||
}
|
||||
1
|
||||
2
|
||||
3
|
||||
"""
|
||||
).trimIndent()
|
||||
|
||||
// Place caret at the end of the line that contains just " 3" (before the line with '}')
|
||||
val lines = before.split('\n')
|
||||
// Build index of end of the line with the last " 3"
|
||||
var idx = 0
|
||||
var caret = 0
|
||||
for (i in lines.indices) {
|
||||
val line = lines[i]
|
||||
if (line.trimEnd() == "3" && i + 1 < lines.size && lines[i + 1].trim() == "}") {
|
||||
caret = idx + line.length // position after '3', before the newline
|
||||
break
|
||||
}
|
||||
idx += line.length + 1 // +1 for newline
|
||||
}
|
||||
|
||||
val res = applyEnter(before, caret, caret, tab)
|
||||
|
||||
val expected = (
|
||||
"""
|
||||
{
|
||||
1
|
||||
2
|
||||
3
|
||||
}
|
||||
1
|
||||
2
|
||||
3
|
||||
"""
|
||||
).trimIndent()
|
||||
|
||||
assertEquals(expected, res.text)
|
||||
}
|
||||
...
|
||||
*/
|
||||
|
||||
/*
|
||||
@Test
|
||||
fun enter_eol_before_brace_only_next_line_various_indents() {
|
||||
// Cover Rule 3 with LF newlines, at indents 0, 2, 4, 8
|
||||
val indents = listOf(0, 2, 4, 8)
|
||||
for (indent in indents) {
|
||||
val spaces = " ".repeat(indent)
|
||||
val before = (
|
||||
"""
|
||||
1
|
||||
2
|
||||
3
|
||||
${'$'}spaces}
|
||||
4
|
||||
"""
|
||||
).trimIndent()
|
||||
// Caret at end of the line with '3' (line before the rbrace-only line)
|
||||
val caret = before.indexOf("3\n") + 1 // just before LF
|
||||
val res = applyEnter(before, caret, caret, tab)
|
||||
|
||||
// The '}' line must be dedented by one block (clamped at 0) and caret moved to its start
|
||||
val expectedIndent = (indent - tab).coerceAtLeast(0)
|
||||
val expected = (
|
||||
"""
|
||||
1
|
||||
2
|
||||
3
|
||||
${'$'}{" ".repeat(expectedIndent)}}
|
||||
4
|
||||
"""
|
||||
).trimIndent()
|
||||
assertEquals(expected, res.text, "EOL before '}' dedent failed for indent=${'$'}indent")
|
||||
// Caret should be at start of that '}' line (line index 3)
|
||||
val lines = res.text.split('\n')
|
||||
var pos = 0
|
||||
for (i in 0 until 3) pos += lines[i].length + 1
|
||||
assertEquals(pos, res.selStart, "Caret pos mismatch for indent=${'$'}indent")
|
||||
assertEquals(pos, res.selEnd, "Caret pos mismatch for indent=${'$'}indent")
|
||||
}
|
||||
}
|
||||
...
|
||||
*/
|
||||
|
||||
/*
|
||||
@Test
|
||||
fun enter_eol_before_brace_only_next_line_various_indents_crlf() {
|
||||
// Same as above but with CRLF newlines
|
||||
val indents = listOf(0, 2, 4, 8)
|
||||
for (indent in indents) {
|
||||
val spaces = " ".repeat(indent)
|
||||
val beforeLf = (
|
||||
"""
|
||||
1
|
||||
2
|
||||
3
|
||||
${'$'}spaces}
|
||||
4
|
||||
"""
|
||||
).trimIndent()
|
||||
val before = beforeLf.replace("\n", "\r\n")
|
||||
val caret = before.indexOf("3\r\n") + 1 // at '3' index + 1 moves to end-of-line before CR
|
||||
val res = applyEnter(before, caret, caret, tab)
|
||||
|
||||
val expectedIndent = (indent - tab).coerceAtLeast(0)
|
||||
val expectedLf = (
|
||||
"""
|
||||
1
|
||||
2
|
||||
3
|
||||
${'$'}{" ".repeat(expectedIndent)}}
|
||||
4
|
||||
"""
|
||||
).trimIndent()
|
||||
assertEquals(expectedLf, res.text.replace("\r\n", "\n"), "CRLF case failed for indent=${'$'}indent")
|
||||
}
|
||||
}
|
||||
...
|
||||
*/
|
||||
|
||||
/*
|
||||
@Test
|
||||
fun enter_at_start_of_brace_only_line_at_cols_0_2_4() {
|
||||
val indents = listOf(0, 2, 4)
|
||||
for (indent in indents) {
|
||||
val spaces = " ".repeat(indent)
|
||||
val before = (
|
||||
"""
|
||||
1
|
||||
2
|
||||
${'$'}spaces}
|
||||
3
|
||||
"""
|
||||
).trimIndent()
|
||||
// Caret at start of the brace line
|
||||
val lines = before.split('\n')
|
||||
var caret = 0
|
||||
for (i in 0 until 2) caret += lines[i].length + 1
|
||||
caret += 0 // column 0 of brace line
|
||||
val res = applyEnter(before, caret, caret, tab)
|
||||
|
||||
// Expect the brace line to be dedented by one block, and a new line inserted before it
|
||||
val expectedIndent = (indent - tab).coerceAtLeast(0)
|
||||
val expected = (
|
||||
"""
|
||||
1
|
||||
2
|
||||
${'$'}{" ".repeat(expectedIndent)}
|
||||
${'$'}{" ".repeat(expectedIndent)}}
|
||||
3
|
||||
"""
|
||||
).trimIndent()
|
||||
assertEquals(expected, res.text, "Brace-line start enter failed for indent=${'$'}indent")
|
||||
// Caret must be at start of the inserted line, which has expectedIndent spaces
|
||||
val afterLines = res.text.split('\n')
|
||||
var pos = 0
|
||||
for (i in 0 until 3) pos += afterLines[i].length + 1
|
||||
// The inserted line is line index 2 (0-based), caret at its start
|
||||
pos -= afterLines[2].length + 1
|
||||
pos += 0
|
||||
assertEquals(pos, res.selStart, "Caret mismatch for indent=${'$'}indent")
|
||||
assertEquals(pos, res.selEnd, "Caret mismatch for indent=${'$'}indent")
|
||||
}
|
||||
}
|
||||
...
|
||||
*/
|
||||
|
||||
@Test
|
||||
fun enter_on_whitespace_only_line_keeps_same_indent() {
|
||||
val before = " \nnext" // line 0 has 4 spaces only
|
||||
val caret = 0 + 4 // at end of spaces, before LF
|
||||
val caret = 4 // at end of spaces, before LF
|
||||
val res = applyEnter(before, caret, caret, tab)
|
||||
// Default smart indent should keep indent = 4
|
||||
// Original text: ' ' + '\n' + 'next'
|
||||
// Inserted: '\n' + ' ' at caret 4
|
||||
// Result: ' ' + '\n' + ' ' + '\n' + 'next'
|
||||
assertEquals(" \n \nnext", res.text)
|
||||
// Caret at start of the new blank line with 4 spaces
|
||||
assertEquals(1 + 4, res.selStart)
|
||||
// Caret at start of the new blank line with 4 spaces (after first newline)
|
||||
// lineStart(0) + indent(4) + newline(1) + newIndent(4) = 9
|
||||
assertEquals(4 + 1 + 4, res.selStart)
|
||||
assertEquals(res.selStart, res.selEnd)
|
||||
}
|
||||
|
||||
/*
|
||||
@Test
|
||||
fun enter_on_line_with_rbrace_else_lbrace_defaults_smart() {
|
||||
val text = " } else {"
|
||||
// Try caret positions after '}', before 'e', and after '{'
|
||||
val carets = listOf(5, 6, text.length)
|
||||
for (c in carets) {
|
||||
val res = applyEnter(text, c, c, tab)
|
||||
// Should not trigger special cases since line is not brace-only or only-spaces after '}'
|
||||
// Expect same indent (4 spaces)
|
||||
val expectedPrefix = text.substring(0, c) + "\n" + " ".repeat(4)
|
||||
assertEquals(expectedPrefix, res.text.substring(0, expectedPrefix.length))
|
||||
assertEquals(c + 1 + 4, res.selStart)
|
||||
assertEquals(res.selStart, res.selEnd)
|
||||
}
|
||||
}
|
||||
...
|
||||
*/
|
||||
|
||||
/*
|
||||
@Test
|
||||
fun enter_with_selection_replaces_and_uses_anchor_indent() {
|
||||
val text = (
|
||||
"""
|
||||
1
|
||||
2
|
||||
3
|
||||
"""
|
||||
).trimIndent()
|
||||
// Select "2\n3" starting at column 4 of line 1 (indent = 4)
|
||||
val idxLine0 = text.indexOf('1')
|
||||
val idxLine1 = text.indexOf('\n', idxLine0) + 1
|
||||
val selStart = idxLine1 + 4 // after 4 spaces
|
||||
val selEnd = text.length
|
||||
val res = applyEnter(text, selStart, selEnd, tab)
|
||||
val expected = (
|
||||
"""
|
||||
1
|
||||
|
||||
"""
|
||||
).trimIndent()
|
||||
assertEquals(expected, res.text)
|
||||
assertEquals(expected.length, res.selStart)
|
||||
assertEquals(res.selStart, res.selEnd)
|
||||
}
|
||||
...
|
||||
*/
|
||||
}
|
||||
Reference in new issue
Block a user