Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ package org.jetbrains.plugins.ideavim.action.change.insert
import com.maddyhome.idea.vim.state.mode.Mode
import org.jetbrains.plugins.ideavim.VimTestCase
import org.junit.jupiter.api.Test
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.ValueSource

class InsertExitModeActionTest : VimTestCase() {
@Test
Expand All @@ -22,4 +24,73 @@ class InsertExitModeActionTest : VimTestCase() {
fun `test exit visual mode on line start`() {
doTest("i<Esc>", "${c}123", "${c}123", Mode.NORMAL())
}

@ParameterizedTest
@ValueSource(strings = ["i", "a", "o", "O"])
fun `test read-only file allows insert entry but blocks changes`(insertCommand: String) {
configureByText("12${c}3")
fixture.editor.document.setReadOnly(true)

// Enter insert-like commands, then immediately escape; no text should change and ESC should work
typeText("$insertCommand<Esc>")

assertMode(Mode.NORMAL())
assertState("12${c}3")
}

@Test
fun `test cannot change text in read-only file with c motion`() {
configureByText("12${c}3")
fixture.editor.document.setReadOnly(true)

// Try to change with 'cw' - should prevent the change
typeText("cw")

// Should remain in normal mode
assertMode(Mode.NORMAL())
assertState("12${c}3")
}

@Test
fun `test cannot change text in read-only file with r motion`() {
configureByText("12${c}3")
fixture.editor.document.setReadOnly(true)

// Try to change with 'r' - should prevent the change
typeText("rX")

// Should remain in normal mode
assertMode(Mode.NORMAL())
assertState("12${c}3")

}

@Test
fun `test exit insert mode in read-only file`() {
// Test that ESC works in read-only files without hanging
configureByText("test ${c}content")
fixture.editor.document.setReadOnly(true)

// This should complete without hanging or errors
typeText("i<Esc>")

// Reset read-only status
fixture.editor.document.setReadOnly(false)
}

@Test
fun `test exit insert mode with repeat count in read-only file`() {
// Test that ESC works with repeat counts in read-only files
configureByText("${c}hello")
fixture.editor.document.setReadOnly(true)

// 3i should repeat the insert 3 times when ESC is pressed
// In read-only files, this should exit cleanly without hanging
typeText("3i<Esc>")
assertMode(Mode.NORMAL())
assertState("${c}hello")

// Reset read-only status
fixture.editor.document.setReadOnly(false)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,18 @@ import com.intellij.vim.annotations.CommandOrMotion
import com.intellij.vim.annotations.Mode
import com.maddyhome.idea.vim.api.ExecutionContext
import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.command.Command
import com.maddyhome.idea.vim.command.OperatorArguments
import com.maddyhome.idea.vim.handler.VimActionHandler
import com.maddyhome.idea.vim.api.VimMarkService
import com.maddyhome.idea.vim.mark.VimMarkConstants.MARK_CHANGE_END
import com.maddyhome.idea.vim.state.mode.Mode as VimMode

@CommandOrMotion(keys = ["<C-[>", "<C-C>", "<Esc>"], modes = [Mode.INSERT])
class InsertExitModeAction : VimActionHandler.SingleExecution() {
// Note that hitting Escape can insert text when exiting insert mode after visual block mode.
// If the editor is read-only, we'll get a "This view is read-only" tooltip. However, we should only enter insert
// mode if both editor and document are writable.
override val type: Command.Type = Command.Type.INSERT
// Note: ESC should not require write access itself; any write is gated in processEscape.
override val type: Command.Type = Command.Type.OTHER_SELF_SYNCHRONIZED

override fun execute(
editor: VimEditor,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ private fun insertNewLineAbove(editor: VimEditor, context: ExecutionContext) {
*/
private fun insertNewLineBelow(editor: VimEditor, context: ExecutionContext) {
if (editor.isOneLineMode()) return

for (caret in editor.nativeCarets()) {
caret.moveToOffset(injector.motion.moveCaretToCurrentLineEnd(editor, caret))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,13 @@ abstract class VimChangeGroupBase : VimChangeGroup {
* @param context The data context
*/
override fun insertAfterCaret(editor: VimEditor, context: ExecutionContext) {
// Prevent entering insert mode in read-only files before moving the caret
if (!editor.isWritable()) {
injector.messages.showStatusBarMessage(editor, "Cannot make changes, file is read-only")
injector.messages.indicateError()
return
}

for (caret in editor.nativeCarets()) {
caret.moveToMotion(injector.motion.getHorizontalMotion(editor, caret, 1, true))
}
Expand Down Expand Up @@ -441,6 +448,13 @@ abstract class VimChangeGroupBase : VimChangeGroup {
* @param mode The mode - indicate insert or replace
*/
override fun initInsert(editor: VimEditor, context: ExecutionContext, mode: Mode) {
// Prevent entering insert mode in read-only files
if (!editor.isWritable()) {
injector.messages.showStatusBarMessage(editor, "Cannot make changes, file is read-only")
injector.messages.indicateError()
return
}

val state = injector.vimState
injector.application.runReadAction {
for (caret in editor.nativeCarets()) {
Expand Down
Loading