Skip to content

Commit

Permalink
VIM-1472 Add support for sorting with pattern
Browse files Browse the repository at this point in the history
  • Loading branch information
Parker7123 committed Feb 12, 2024
1 parent 89a24d7 commit ed38c4c
Show file tree
Hide file tree
Showing 3 changed files with 263 additions and 35 deletions.
70 changes: 43 additions & 27 deletions src/main/java/com/maddyhome/idea/vim/group/ChangeGroup.kt
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ import com.maddyhome.idea.vim.newapi.IjEditorExecutionContext
import com.maddyhome.idea.vim.newapi.IjVimCaret
import com.maddyhome.idea.vim.newapi.IjVimEditor
import com.maddyhome.idea.vim.newapi.ij
import com.maddyhome.idea.vim.regexp.VimRegex
import com.maddyhome.idea.vim.regexp.match.VimMatchResult
import com.maddyhome.idea.vim.state.mode.Mode
import com.maddyhome.idea.vim.state.mode.Mode.VISUAL
import com.maddyhome.idea.vim.state.mode.SelectionType
Expand Down Expand Up @@ -577,48 +579,62 @@ public class ChangeGroup : VimChangeGroupBase() {
}
val startOffset = editor.getLineStartOffset(startLine)
val endOffset = editor.getLineEndOffset(endLine)
return sortTextRange(editor, caret, startOffset, endOffset, lineComparator, sortOptions)
}

/**
* Sorts a text range with a comparator. Returns true if a replace was performed, false otherwise.
*
* @param editor The editor to replace text in
* @param start The starting position for the sort
* @param end The ending position for the sort
* @param lineComparator The comparator to use to sort
* @param sortOption The option to sort the range
* @return true if able to sort the text, false if not
*/
private fun sortTextRange(
editor: VimEditor,
caret: VimCaret,
start: Int,
end: Int,
lineComparator: Comparator<String>,
sortOption: SortOption,
): Boolean {
val selectedText = (editor as IjVimEditor).editor.document.getText(TextRangeInterval(start, end))
val lines: MutableList<String> = selectedText.split("\n").sortedWith(lineComparator).toMutableList()
if (sortOption.unique) {
val iterator = lines.iterator()
val selectedText = (editor as IjVimEditor).editor.document.getText(TextRangeInterval(startOffset, endOffset))
val lines = selectedText.split("\n")
val modifiedLines = sortOptions.pattern?.let {
if (sortOptions.sortOnPattern) {
extractPatternFromLines(editor, lines, startLine, it)
} else {
deletePatternFromLines(editor, lines, startLine, it)
}
} ?: lines
val sortedLines = lines.zip(modifiedLines)
.sortedWith { l1, l2 -> lineComparator.compare(l1.second, l2.second) }
.map {it.first}
.toMutableList()

if (sortOptions.unique) {
val iterator = sortedLines.iterator()
var previous: String? = null
while (iterator.hasNext()) {
val current = iterator.next()
if (current == previous || sortOption.ignoreCase && current.equals(previous, ignoreCase = true)) {
if (current == previous || sortOptions.ignoreCase && current.equals(previous, ignoreCase = true)) {
iterator.remove()
} else {
previous = current
}
}
}
if (lines.size < 1) {
if (sortedLines.isEmpty()) {
return false
}
replaceText(editor, caret, start, end, StringUtil.join(lines, "\n"))
replaceText(editor, caret, startOffset, endOffset, StringUtil.join(sortedLines, "\n"))
return true
}

private fun extractPatternFromLines(editor: VimEditor, lines: List<String>, startLine: Int, pattern: String): List<String> {
val regex = VimRegex(pattern)
return lines.mapIndexed { i: Int, line: String ->
val result = regex.findInLine(editor, startLine + i, 0)
when (result) {
is VimMatchResult.Success -> result.value
is VimMatchResult.Failure -> line
}
}
}

private fun deletePatternFromLines(editor: VimEditor, lines: List<String>, startLine: Int, pattern: String): List<String> {
val regex = VimRegex(pattern)
return lines.mapIndexed { i: Int, line: String ->
val result = regex.findInLine(editor, startLine + i, 0)
when (result) {
is VimMatchResult.Success -> line.substring(result.value.length, line.length)
is VimMatchResult.Failure -> line
}
}
}

/**
* Perform increment and decrement for numbers in visual mode
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -592,6 +592,194 @@ class SortCommandTest : VimTestCase() {
)
)
}

@JvmStatic
fun patternTestCases(): List<TestCase> {
return listOf(
TestCase(
// skip first character
sortCommand = "sort /./",
content = """
'
a
ab
aBc
a122
b123
c121
""".trimIndent(),
expected = """
'
a
c121
a122
b123
aBc
ab
""".trimIndent()
),
TestCase(
// skip first character reversed
sortCommand = "sort! /./",
content = """
'
a
ab
abc
a122
b123
c121
""".trimIndent(),
expected = """
abc
ab
b123
a122
c121
'
a
""".trimIndent()
),
TestCase(
// skip first character case-insensitive
sortCommand = "sort /./ i",
content = """
'
a
ab
aBc
a122
b123
c121
""".trimIndent(),
expected = """
'
a
c121
a122
b123
ab
aBc
""".trimIndent()
),
TestCase(
// skip first character numeric sort
sortCommand = "sort /./ n",
content = """
'
a
a122
b2
c121
""".trimIndent(),
expected = """
'
a
b2
c121
a122
""".trimIndent()
),
TestCase(
// sort on first character
sortCommand = "sort /./ r",
content = """
'
baa
azz
abb
aaa
""".trimIndent(),
expected = """
'
azz
abb
aaa
baa
""".trimIndent()
),
TestCase(
// numeric sort skip first digit
sortCommand = "sort /\\d/ n",
content = """
190
270
350
410
""".trimIndent(),
expected = """
410
350
270
190
""".trimIndent()
),
TestCase(
// numeric sort on first digit
sortCommand = "sort /\\d/ nr",
content = """
10
90
100
700
""".trimIndent(),
expected = """
10
100
700
90
""".trimIndent()
),
TestCase(
// sort on third virtual column
sortCommand = "sort /.*\\%3v/",
content = """
aad
bbc
ccb
dda
""".trimIndent(),
expected = """
dda
ccb
bbc
aad
""".trimIndent()
),
TestCase(
// sort on second comma separated field
sortCommand = "sort /[^,]*/",
content = """
aaa,ddd
bbb,ccc
ccc,bbb
ddd,aaa
""".trimIndent(),
expected = """
ddd,aaa
ccc,bbb
bbb,ccc
aaa,ddd
""".trimIndent()
),
TestCase(
// sort on first number in line
sortCommand = "sort /.\\{-}\\ze\\d/ n",
content = """
aaaa9
b10
ccccc7
dd3
""".trimIndent(),
expected = """
dd3
ccccc7
aaaa9
b10
""".trimIndent()
)
)
}
}


Expand Down Expand Up @@ -639,6 +827,12 @@ class SortCommandTest : VimTestCase() {
testCase: TestCase,
) = assertSort(testCase)

@ParameterizedTest
@MethodSource("patternTestCases")
fun `test sort with pattern`(
testCase: TestCase,
) = assertSort(testCase)

@Test
fun testSortWithPrecedingWhiteSpace() {
configureByText(" zee\n c\n a\n b\n whatever")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,7 @@ public data class SortCommand(val ranges: Ranges, val argument: String) : Comman

@Throws(ExException::class)
override fun processCommand(editor: VimEditor, context: ExecutionContext, operatorArguments: OperatorArguments): ExecutionResult {
val arg = argument
val nonEmptyArg = arg.trim().isNotEmpty()
val sortOption = SortOption(
reverse = nonEmptyArg && "!" in arg,
ignoreCase = nonEmptyArg && "i" in arg,
numeric = nonEmptyArg && "n" in arg,
unique = nonEmptyArg && "u" in arg,
)
val sortOption = parseSortOption(argument)
val lineComparator = LineComparator(sortOption.ignoreCase, sortOption.numeric, sortOption.reverse)
if (editor.inBlockSelection) {
val primaryCaret = editor.primaryCaret()
Expand Down Expand Up @@ -89,6 +82,29 @@ public data class SortCommand(val ranges: Ranges, val argument: String) : Comman
return normalizedRange
}

private fun parseSortOption(arg: String): SortOption {
val patternRange = extractPattern(arg)
val pattern = patternRange?.let { arg.substring(it) }
val flags = patternRange?.let { arg.removeRange(patternRange)} ?: arg
return SortOption(
reverse = "!" in flags,
ignoreCase = "i" in flags,
numeric = "n" in flags,
unique = "u" in flags,
sortOnPattern = "r" in flags,
pattern = pattern
)
}

private fun extractPattern(arg: String): IntRange? {
val startIndex = arg.indexOf('/',)
val endIndex = arg.indexOf('/', startIndex + 2)
if (startIndex >= 0 && endIndex >= 0) {
return IntRange(startIndex + 1, endIndex - 1)
}
return null
}

private class LineComparator(
private val ignoreCase: Boolean,
private val numeric: Boolean,
Expand Down Expand Up @@ -131,4 +147,6 @@ public data class SortOption(
val numeric: Boolean,
val reverse: Boolean,
val unique: Boolean,
val sortOnPattern: Boolean,
val pattern: String? = null
)

0 comments on commit ed38c4c

Please sign in to comment.