Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

VIM-1472 Add support for sorting with pattern #820

Merged
merged 1 commit into from
Feb 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
)
Loading