1
0
mirror of https://github.com/chylex/IntelliJ-IdeaVim.git synced 2025-05-05 18:34:03 +02:00

Rewrite displayLocationInfo to avoid findNextWord

Simplifies and fixes counting words, especially when selection intersects words. Also moves implementation to engine and adds more tests.
This commit is contained in:
Matt Ellis 2025-02-07 10:19:05 +00:00 committed by Alex Pláte
parent 8eef802ac7
commit b5937e885d
5 changed files with 313 additions and 193 deletions
src
main/java/com/maddyhome/idea/vim
test/java/org/jetbrains/plugins/ideavim/action
vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api

View File

@ -32,17 +32,13 @@ import com.maddyhome.idea.vim.api.ExecutionContext
import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.api.VimFileBase
import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.common.TextRange
import com.maddyhome.idea.vim.group.LastTabService.Companion.getInstance
import com.maddyhome.idea.vim.helper.EditorHelper
import com.maddyhome.idea.vim.helper.MessageHelper.message
import com.maddyhome.idea.vim.helper.countWords
import com.maddyhome.idea.vim.helper.fileSize
import com.maddyhome.idea.vim.newapi.IjEditorExecutionContext
import com.maddyhome.idea.vim.newapi.IjVimEditor
import com.maddyhome.idea.vim.newapi.execute
import com.maddyhome.idea.vim.newapi.globalIjOptions
import com.maddyhome.idea.vim.state.mode.Mode.VISUAL
import java.io.File
import java.util.*
@ -265,92 +261,6 @@ class FileGroup : VimFileBase() {
return null
}
override fun displayLocationInfo(vimEditor: VimEditor) {
val editor = (vimEditor as IjVimEditor).editor
val msg = StringBuilder()
val doc = editor.document
if (injector.vimState.mode !is VISUAL) {
val lp = editor.caretModel.logicalPosition
val col = editor.caretModel.offset - doc.getLineStartOffset(lp.line)
var endoff = doc.getLineEndOffset(lp.line)
if (endoff < editor.fileSize && doc.charsSequence[endoff] == '\n') {
endoff--
}
val ecol = endoff - doc.getLineStartOffset(lp.line)
val elp = editor.offsetToLogicalPosition(endoff)
msg.append("Col ").append(col + 1)
if (col != lp.column) {
msg.append("-").append(lp.column + 1)
}
msg.append(" of ").append(ecol + 1)
if (ecol != elp.column) {
msg.append("-").append(elp.column + 1)
}
val lline = editor.caretModel.logicalPosition.line
val total = IjVimEditor(editor).lineCount()
msg.append("; Line ").append(lline + 1).append(" of ").append(total)
val cp = countWords(vimEditor)
msg.append("; Word ").append(cp.position).append(" of ").append(cp.count)
val offset = editor.caretModel.offset
val size = editor.fileSize
msg.append("; Character ").append(offset + 1).append(" of ").append(size)
} else {
msg.append("Selected ")
val vr = TextRange(
editor.selectionModel.blockSelectionStarts,
editor.selectionModel.blockSelectionEnds
)
vr.normalize()
val lines: Int
var cp = countWords(vimEditor)
val words = cp.count
var word = 0
if (vr.isMultiple) {
lines = vr.size()
val cols = vr.maxLength
msg.append(cols).append(" Cols; ")
for (i in 0 until vr.size()) {
cp = countWords(vimEditor, vr.startOffsets[i], (vr.endOffsets[i] - 1).toLong())
word += cp.count
}
} else {
val slp = editor.offsetToLogicalPosition(vr.startOffset)
val elp = editor.offsetToLogicalPosition(vr.endOffset)
lines = elp.line - slp.line + 1
cp = countWords(vimEditor, vr.startOffset, (vr.endOffset - 1).toLong())
word = cp.count
}
val total = IjVimEditor(editor).lineCount()
msg.append(lines).append(" of ").append(total).append(" Lines")
msg.append("; ").append(word).append(" of ").append(words).append(" Words")
val chars = vr.selectionCount
val size = editor.fileSize
msg.append("; ").append(chars).append(" of ").append(size).append(" Characters")
}
VimPlugin.showMessage(msg.toString())
}
override fun displayFileInfo(vimEditor: VimEditor, fullPath: Boolean) {
val editor = (vimEditor as IjVimEditor).editor
val msg = StringBuilder()

View File

@ -10,11 +10,9 @@ package com.maddyhome.idea.vim.helper
import com.intellij.codeInsight.daemon.impl.DaemonCodeAnalyzerEx
import com.intellij.codeInsight.daemon.impl.HighlightInfo
import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.editor.Caret
import com.intellij.openapi.editor.Editor
import com.intellij.spellchecker.SpellCheckerSeveritiesProvider
import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.api.getLineEndOffset
import com.maddyhome.idea.vim.api.globalOptions
import com.maddyhome.idea.vim.api.injector
@ -52,48 +50,6 @@ private fun containsUpperCase(pattern: String): Boolean {
return false
}
/**
* This counts all the words in the file.
*/
fun countWords(
vimEditor: VimEditor,
start: Int = 0,
end: Long = vimEditor.fileSize(),
): CountPosition {
val offset = vimEditor.currentCaret().offset
var count = 1
var position = 0
var last = -1
var res = start
while (true) {
res = injector.searchHelper.findNextWord(vimEditor, res, 1, true, false)
if (res == start || res == 0 || res > end || res == last) {
break
}
count++
if (res == offset) {
position = count
} else if (last < offset && res >= offset) {
position = if (count == 2) {
1
} else {
count - 1
}
}
last = res
}
if (position == 0 && res == offset) {
position = count
}
return CountPosition(count, position)
}
/**
* Find the word under the cursor or the next word to the right of the cursor on the current line.
*
@ -193,9 +149,3 @@ private fun skip(iterator: IntIterator, n: Int) {
var i = n
while (i-- != 0 && iterator.hasNext()) iterator.nextInt()
}
class CountPosition(val count: Int, val position: Int)
private val logger = logger<SearchLogger>()
private class SearchLogger

View File

@ -9,80 +9,231 @@
package org.jetbrains.plugins.ideavim.action
import com.maddyhome.idea.vim.VimPlugin
import com.maddyhome.idea.vim.api.injector
import org.jetbrains.plugins.ideavim.VimBehaviorDiffers
import org.jetbrains.plugins.ideavim.VimTestCase
import org.junit.jupiter.api.Test
import kotlin.test.assertEquals
// For all of these tests, note that Vim might show a different total byte count - off by one. This is ok, and not worth
// adding a VimBehaviorDiffers annotation for.
// It's because Vim requires each line to end with a linefeed character (otherwise it's not a line!) and adds one to
// the last line. If the last line ends with a linefeed, that's just the end of the line. In this case, Vim does not
// draw an empty line after the last line (because there isn't one!). If we hit enter at the end of the last line, Vim
// adds a second linefeed, and there's now a new (empty) line at the end of the file, and the file ends with two
// linefeed characters.
// IntelliJ treats a linefeed at the end of the last line as a line feed, and draws an empty line. When we initialise
// a test with a trailing empty line, IntelliJ only creates one linefeed char, instead of the two that Vim creates.
// Maybe we should ensure that each file ends with a linefeed when initialising tests?
@Suppress("SpellCheckingInspection")
class FileGetLocationInfoActionTest : VimTestCase() {
@VimBehaviorDiffers(originalVimAfter = "Col 1 of 11; Line 1 of 6; Word 1 of 32; Byte 1 of 166")
@Test
fun `test get file info`() {
val keys = injector.parser.parseKeys("g<C-G>")
val before = """
${c}Lorem Ipsum
Lorem ipsum dolor sit amet,
consectetur adipiscing elit
Sed in orci mauris.
Cras id tellus in ex imperdiet egestas.
""".trimIndent()
|${c}Lorem Ipsum
|
|Lorem ipsum dolor sit amet,
|consectetur adipiscing elit
|Sed in orci mauris.
|Cras id tellus in ex imperdiet egestas.
""".trimMargin()
configureByText(before)
typeText(keys)
kotlin.test.assertEquals("Col 1 of 11; Line 1 of 6; Word 1 of 23; Character 1 of 128", VimPlugin.getMessage())
typeText("g<C-G>")
assertEquals("Col 1 of 11; Line 1 of 6; Word 1 of 21; Byte 1 of 128", VimPlugin.getMessage())
}
@VimBehaviorDiffers(originalVimAfter = "Col 1 of 11; Line 1 of 7; Word 1 of 32; Byte 1 of 167")
@Test
fun `test get file info with empty line`() {
val keys = injector.parser.parseKeys("g<C-G>")
val before = """
${c}Lorem Ipsum
Lorem ipsum dolor sit amet,
consectetur adipiscing elit
Sed in orci mauris.
Cras id tellus in ex imperdiet egestas.
""".trimIndent()
configureByText(before)
typeText(keys)
kotlin.test.assertEquals("Col 1 of 11; Line 1 of 7; Word 1 of 24; Character 1 of 129", VimPlugin.getMessage())
fun `test get file info with single word`() {
configureByText("Lorem")
typeText("g<C-G>")
assertEquals("Col 1 of 5; Line 1 of 1; Word 1 of 1; Byte 1 of 5", VimPlugin.getMessage())
}
@Test
fun `test get file info of two separate words`() {
configureByText("Lorem ipsum")
typeText("g<C-G>")
assertEquals("Col 1 of 11; Line 1 of 1; Word 1 of 2; Byte 1 of 11", VimPlugin.getMessage())
}
@Test
fun `test get file info of one WORD containing non-word characters`() {
configureByText("Lorem,,,,,ipsum")
typeText("g<C-G>")
assertEquals("Col 1 of 15; Line 1 of 1; Word 1 of 1; Byte 1 of 15", VimPlugin.getMessage())
}
@Test
fun `test get file info of words on multiple lines`() {
val before = """
|Lorem ipsum dolor sit amet
|cons${c}ectetur adipiscing elit
""".trimMargin()
configureByText(before)
typeText("g<C-G>")
assertEquals("Col 5 of 27; Line 2 of 2; Word 6 of 8; Byte 32 of 54", VimPlugin.getMessage())
}
@Test
fun `test get file info of words with trailing punctuation on multiple lines`() {
val before = """
|Lorem ipsum dolor sit amet,
|cons${c}ectetur adipiscing elit
""".trimMargin()
configureByText(before)
typeText("g<C-G>")
assertEquals("Col 5 of 27; Line 2 of 2; Word 6 of 8; Byte 33 of 55", VimPlugin.getMessage())
}
@Test
fun `test get file info of words with empty lines`() {
val before = """
|Lorem ipsum dolor sit amet,
|
|
|${c}
|consectetur adipiscing elit
""".trimMargin()
configureByText(before)
typeText("g<C-G>")
assertEquals("Col 1 of 0; Line 4 of 5; Word 5 of 8; Byte 31 of 58", VimPlugin.getMessage())
}
@Test
fun `test get file info on empty line shows zero columns`() {
val before = """
|Lorem Ipsum
|${c}
|Lorem ipsum dolor sit amet,
|consectetur adipiscing elit
|Sed in orci mauris.
|Cras id tellus in ex imperdiet egestas.
|
""".trimMargin()
configureByText(before)
typeText("g<C-G>")
assertEquals("Col 1 of 0; Line 2 of 7; Word 2 of 21; Byte 13 of 129", VimPlugin.getMessage())
}
@VimBehaviorDiffers(originalVimAfter = "Col 1 of 40; Line 4 of 7; Word 12 of 32; Byte 55 of 167")
@Test
fun `test get file info in the middle`() {
val keys = injector.parser.parseKeys("g<C-G>")
val before = """
Lorem Ipsum
Lorem ipsum dolor sit amet,
all rocks ${c}and lavender and tufted grass,
Sed in orci mauris.
Cras id tellus in ex imperdiet egestas.
""".trimIndent()
|Lorem Ipsum
|
|Lorem ipsum dolor sit amet,
|consectetur ${c}adipiscing elit
|Sed in orci mauris.
|Cras id tellus in ex imperdiet egestas.
|
""".trimMargin()
configureByText(before)
typeText(keys)
kotlin.test.assertEquals("Col 11 of 40; Line 4 of 7; Word 11 of 28; Character 52 of 142", VimPlugin.getMessage())
typeText("g<C-G>")
assertEquals("Col 13 of 27; Line 4 of 7; Word 9 of 21; Byte 54 of 129", VimPlugin.getMessage())
}
@VimBehaviorDiffers(originalVimAfter = "Col 1 of 0; Line 7 of 7; Word 32 of 32; Byte 167 of 167")
@Test
fun `test get file info on the last line`() {
val keys = injector.parser.parseKeys("g<C-G>")
val before = """
Lorem Ipsum
Lorem ipsum dolor sit amet,
consectetur adipiscing elit
Sed in orci mauris.
Cras id tellus in ex imperdiet egestas.
$c
""".trimIndent()
|Lorem Ipsum
|
|Lorem ipsum dolor sit amet,
|consectetur adipiscing elit
|Sed in orci mauris.
|Cras id tellus in ex imperdiet egestas.
|$c
""".trimMargin()
configureByText(before)
typeText(keys)
kotlin.test.assertEquals("Col 1 of 1; Line 7 of 7; Word 24 of 24; Character 130 of 129", VimPlugin.getMessage())
typeText("g<C-G>")
assertEquals("Col 1 of 0; Line 7 of 7; Word 21 of 21; Byte 130 of 129", VimPlugin.getMessage())
}
@Test
fun `test get file info with single word selected`() {
val before = """
|Lorem ${c}ipsum dolor sit amet
|consectetur adipiscing elit
""".trimMargin()
configureByText(before)
typeText("ve", "g<C-G>")
assertEquals("Selected 1 of 2 Lines; 1 of 8 Words; 5 of 54 Bytes", VimPlugin.getMessage())
}
@Test
fun `test get file info with multiple words selected`() {
val before = """
|Lorem ${c}ipsum dolor sit amet
|consectetur adipiscing elit
""".trimMargin()
configureByText(before)
typeText("v2e", "g<C-G>")
assertEquals("Selected 1 of 2 Lines; 2 of 8 Words; 11 of 54 Bytes", VimPlugin.getMessage())
}
@Test
fun `test get file info with single WORD selected`() {
val before = """
|Lorem ${c}ipsum,,,,dolor sit amet
|consectetur adipiscing elit
""".trimMargin()
configureByText(before)
typeText("vE", "g<C-G>")
assertEquals("Selected 1 of 2 Lines; 1 of 7 Words; 14 of 57 Bytes", VimPlugin.getMessage())
}
@Test
fun `test get file info across multiple selected lines`() {
val before = """
|Lorem Ipsum
|
|Lorem ${c}ipsum dolor sit amet,
|consectetur adipiscing elit
|Sed in orci mauris.
|Cras id tellus in ex imperdiet egestas.
""".trimMargin()
configureByText(before)
typeText("v", "jj", "g<C-G>")
assertEquals("Selected 3 of 6 Lines; 9 of 21 Words; 57 of 128 Bytes", VimPlugin.getMessage())
}
@Test
fun `test get file info with empty lines selected`() {
val before = """
|Lorem ${c}ipsum dolor sit amet,
|
|
|
|consectetur adipiscing elit
""".trimMargin()
configureByText(before)
typeText("vG", "g<C-G>")
assertEquals("Selected 5 of 5 Lines; 5 of 8 Words; 26 of 58 Bytes", VimPlugin.getMessage())
}
@Test
fun `test get file info with linewise selection`() {
val before = """
|Lorem Ipsum
|
|Lorem ${c}ipsum dolor sit amet,
|consectetur adipiscing elit
|Sed in orci mauris.
|Cras id tellus in ex imperdiet egestas.
""".trimMargin()
configureByText(before)
typeText("V", "jj", "g<C-G>")
assertEquals("Selected 3 of 6 Lines; 12 of 21 Words; 76 of 128 Bytes", VimPlugin.getMessage())
}
@Test
fun `test get file info with blockwise selection`() {
val before = """
|Lorem Ipsum
|
|Lorem ${c}ipsum dolor sit amet,
|consectetur adipiscing elit
|Sed in orci mauris.
|Cras id tellus in ex imperdiet egestas.
""".trimMargin()
configureByText(before)
typeText("<C-V>", "jjj", "llll", "g<C-G>")
assertEquals("Selected 5 Cols; 4 of 6 Lines; 5 of 21 Words; 20 of 128 Bytes", VimPlugin.getMessage())
}
}

View File

@ -11,7 +11,7 @@ package com.maddyhome.idea.vim.api
interface VimFile {
fun displayFileInfo(vimEditor: VimEditor, fullPath: Boolean)
fun displayHexInfo(editor: VimEditor)
fun displayLocationInfo(vimEditor: VimEditor)
fun displayLocationInfo(editor: VimEditor)
fun selectPreviousTab(context: ExecutionContext)
fun saveFile(editor: VimEditor, context: ExecutionContext)
fun saveFiles(editor: VimEditor, context: ExecutionContext)

View File

@ -8,6 +8,13 @@
package com.maddyhome.idea.vim.api
import com.maddyhome.idea.vim.group.visual.VimSelection
import com.maddyhome.idea.vim.helper.CharacterHelper
import com.maddyhome.idea.vim.helper.CharacterHelper.charType
import com.maddyhome.idea.vim.helper.endOffsetInclusive
import com.maddyhome.idea.vim.state.mode.SelectionType.CHARACTER_WISE
import com.maddyhome.idea.vim.state.mode.inVisualMode
import com.maddyhome.idea.vim.state.mode.selectionType
import java.lang.Long.toHexString
abstract class VimFileBase : VimFile {
@ -17,4 +24,106 @@ abstract class VimFileBase : VimFile {
injector.messages.showStatusBarMessage(editor, toHexString(ch.code.toLong()))
}
override fun displayLocationInfo(editor: VimEditor) {
val msg = buildString {
val caret = editor.currentCaret()
val offset = editor.currentCaret().offset
val totalByteCount = editor.fileSize()
val totalWordCount = countBigWords(editor, offset)
if (!editor.inVisualMode) {
val pos = caret.getBufferPosition()
val line = pos.line + 1
val col = pos.column + 1
val lineEndOffset = editor.getLineEndOffset(pos.line)
val lineEndCol = editor.offsetToBufferPosition(lineEndOffset).column
// Note that Vim can have different screen columns to buffer columns, and displays these in the form "Col 1-3".
// Vim uses screen columns to insert inlay text and symbols such as word wrap indicators, but has a single line
// even when wrapped, unlike IntelliJ, which has a virtual line per screen line. Because of this, it's not clear
// how IdeaVim could represent this, or even if it would be worthwhile to do so.
append("Col ").append(col).append(" of ").append(lineEndCol)
append("; Line ").append(line).append(" of ").append(editor.lineCount())
append("; Word ").append(totalWordCount.currentWord).append(" of ").append(totalWordCount.count)
append("; Byte ").append(offset + 1).append(" of ").append(totalByteCount)
}
else {
append("Selected ")
val selection = VimSelection.create(
caret.vimSelectionStart,
caret.offset,
editor.mode.selectionType ?: CHARACTER_WISE,
editor
).toVimTextRange()
val selectedLineCount: Int
val selectedWordCount: Int
if (selection.isMultiple) {
selectedLineCount = selection.size()
var count = 0
for (i in 0 until selection.size()) {
val wordCount = countBigWords(editor, offset, selection.startOffsets[i], selection.endOffsets[i])
count += wordCount.count
}
selectedWordCount = count
append(selection.maxLength).append(" Cols; ")
}
else {
val startPos = editor.offsetToBufferPosition(selection.startOffset)
val endPos = editor.offsetToBufferPosition(selection.endOffsetInclusive)
selectedLineCount = endPos.line - startPos.line + 1
val wordCount = countBigWords(editor, offset, selection.startOffset, selection.endOffset)
selectedWordCount = wordCount.count
}
append(selectedLineCount).append(" of ").append(editor.lineCount()).append(" Lines; ")
append(selectedWordCount).append(" of ").append(totalWordCount.count).append(" Words; ")
append(selection.selectionCount).append(" of ").append(totalByteCount).append(" Bytes")
}
}
injector.messages.showStatusBarMessage(editor, msg)
}
}
/**
* Count the number of WORDs that intersect the given range, or exist in the whole file
*/
private fun countBigWords(
editor: VimEditor,
caretOffset: Int,
start: Int = 0,
endExclusive: Int = editor.text().length,
): WordCount {
val chars = editor.text()
var wordCount = 0
var currentWord = 0
var lastCharacterType: CharacterHelper.CharacterType? = null
for (pos in start until endExclusive) {
val characterType = charType(editor, chars[pos], punctuationAsLetters = true)
if (characterType != lastCharacterType && characterType != CharacterHelper.CharacterType.WHITESPACE) {
wordCount++
}
// The current word is the last counted word
if (pos <= caretOffset) {
currentWord = wordCount
}
lastCharacterType = characterType
}
return WordCount(wordCount, currentWord)
}
private data class WordCount(val count: Int, val currentWord: Int)