1
0
mirror of https://github.com/chylex/IntelliJ-IdeaVim.git synced 2025-07-30 09:59:08 +02:00

Variable locking

This commit is contained in:
lippfi 2021-10-20 13:22:08 +03:00
parent fc81c6329b
commit 0da18b81b6
14 changed files with 347 additions and 1 deletions
src
main
test/java/org/jetbrains/plugins/ideavim/ex/implementation/commands

View File

@ -114,7 +114,7 @@ command:
| MARK_COMMAND | JUMPS | J_LOWERCASE | JOIN_LINES | HISTORY | GO_TO_CHAR | SYMBOL | FIND | CLASS | F_LOWERCASE
| FILE | EXIT | E_LOWERCASE | EDIT_FILE | DUMP_LINE | DIGRAPH | DEL_MARKS | D_LOWERCASE | DEL_LINES | DELCMD
| T_LOWERCASE | COPY | CMD_CLEAR | EXCLAMATION | BUFFER_LIST | BUFFER_CLOSE | B_LOWERCASE | BUFFER | ASCII
| ACTIONLIST | ACTION
| ACTIONLIST | ACTION | LOCKVAR | UNLOCKVAR
)
WS* ((commandArgumentWithoutBars? inline_comment NEW_LINE) | (commandArgumentWithoutBars? NEW_LINE) | (commandArgumentWithoutBars? BAR)) (NEW_LINE | BAR)*
#CommandWithComment|
@ -666,6 +666,8 @@ UNMAP: 'unm' | 'nun' | 'vu' | 'xu' | 'sunm' | 'ou' | 'iu' | 'cu
| 'unma' | 'nunma' | 'vunma' | 'xunma' | 'sunma' | 'ounma' | 'iunma' | 'cunma'
| 'unmap' | 'nunmap' | 'vunmap' | 'xunmap' | 'sunmap' | 'ounmap' | 'iunmap' | 'cunmap';
EXECUTE: 'exe' | 'exec' | 'execu' | 'execut' | 'execute';
LOCKVAR: 'lockv' | 'lockva' | 'lockvar';
UNLOCKVAR: 'unlo' | 'unloc' | 'unlock' | 'unlockv' | 'unlockva' | 'unlockvar';
// Types
DIGIT: [0-9];

View File

@ -55,6 +55,9 @@ data class LetCommand(
throw ExException("E46: Cannot change read-only variable \"${variable.toString(editor, context, parent)}\"")
}
val leftValue = VariableService.getNullableVariableValue(variable, editor, context, parent)
if (leftValue?.isLocked == true && leftValue.lockOwner?.name == variable.name) {
throw ExException("E741: Value is locked: ${variable.toString(editor, context, parent)}")
}
val rightValue = expression.evaluate(editor, context, parent)
VariableService.storeVariable(variable, operator.getNewValue(leftValue, rightValue), editor, context, this)
}
@ -68,8 +71,16 @@ data class LetCommand(
}
val expressionValue = expression.evaluate(editor, context, this)
var valueToStore = if (dictKey in containerValue.dictionary) {
if (containerValue.dictionary[dictKey]!!.isLocked) {
// todo better exception message
throw ExException("E741: Value is locked: ${variable.originalString}")
}
operator.getNewValue(containerValue.dictionary[dictKey]!!, expressionValue)
} else {
if (containerValue.isLocked) {
// todo better exception message
throw ExException("E741: Value is locked: ${variable.originalString}")
}
expressionValue
}
if (valueToStore is VimFuncref && !valueToStore.isSelfFixed &&
@ -88,6 +99,9 @@ data class LetCommand(
if (index > containerValue.values.size - 1) {
throw ExException("E684: list index out of range: $index")
}
if (containerValue.values[index].isLocked) {
throw ExException("E741: Value is locked: ${variable.originalString}")
}
containerValue.values[index] = operator.getNewValue(containerValue.values[index], expression.evaluate(editor, context, parent))
}
is VimBlob -> TODO()

View File

@ -0,0 +1,67 @@
package com.maddyhome.idea.vim.vimscript.model.commands
import com.intellij.openapi.actionSystem.DataContext
import com.intellij.openapi.editor.Editor
import com.maddyhome.idea.vim.ex.ExException
import com.maddyhome.idea.vim.ex.ranges.Ranges
import com.maddyhome.idea.vim.vimscript.model.ExecutionResult
import com.maddyhome.idea.vim.vimscript.model.expressions.Scope
import com.maddyhome.idea.vim.vimscript.model.expressions.Variable
import com.maddyhome.idea.vim.vimscript.services.VariableService
/**
* see :h lockvar
*/
class LockVarCommand(val ranges: Ranges, val argument: String) : Command.SingleExecution(ranges, argument) {
override val argFlags = flags(RangeFlag.RANGE_FORBIDDEN, ArgumentFlag.ARGUMENT_OPTIONAL, Access.READ_ONLY)
// todo doesn't throw proper vim exceptions in case of wrong arguments
override fun processCommand(editor: Editor, context: DataContext): ExecutionResult {
val variableAndDepth = parseVariableAndDepth(argument)
VariableService.lockVariable(variableAndDepth.first, variableAndDepth.second, editor, context, parent)
return ExecutionResult.Success
}
}
/**
* see :h unlockvar
*/
class UnlockVarCommand(val ranges: Ranges, val argument: String) : Command.SingleExecution(ranges, argument) {
override val argFlags = flags(RangeFlag.RANGE_FORBIDDEN, ArgumentFlag.ARGUMENT_OPTIONAL, Access.READ_ONLY)
// todo doesn't throw proper vim exceptions in case of wrong arguments
override fun processCommand(editor: Editor, context: DataContext): ExecutionResult {
val variableAndDepth = parseVariableAndDepth(argument)
VariableService.unlockVariable(variableAndDepth.first, variableAndDepth.second, editor, context, parent)
return ExecutionResult.Success
}
}
private fun parseVariableAndDepth(argument: String): Pair<Variable, Int> {
val variable: String
var arg = argument
var depth = 2
if (arg.startsWith("!")) {
depth = 100
arg = arg.substring(1)
}
val splittedArg = arg.trim().split(" ")
when (splittedArg.size) {
1 -> variable = splittedArg[0]
2 -> {
depth = splittedArg[0].toIntOrNull() ?: 2
variable = splittedArg[1]
}
else -> throw ExException("Unknown error during lockvar command execution")
}
return Pair(parseVariable(variable), depth)
}
private fun parseVariable(variable: String): Variable {
val splittedString = variable.split(":")
return when (splittedString.size) {
1 -> Variable(null, splittedString[0])
2 -> Variable(Scope.getByValue(splittedString[0]), splittedString[1])
else -> throw ExException("Unknown error during lockvar command execution")
}
}

View File

@ -22,6 +22,14 @@ class VimBlob : VimDataType() {
TODO("Not yet implemented")
}
override fun lockVar(depth: Int) {
TODO("Not yet implemented")
}
override fun unlockVar(depth: Int) {
TODO("Not yet implemented")
}
override fun toString(): String {
TODO("Not yet implemented")
}

View File

@ -1,5 +1,7 @@
package com.maddyhome.idea.vim.vimscript.model.datatypes
import com.maddyhome.idea.vim.vimscript.model.expressions.Variable
sealed class VimDataType {
abstract fun asDouble(): Double
@ -19,4 +21,10 @@ sealed class VimDataType {
}
abstract fun deepCopy(level: Int = 100): VimDataType
var lockOwner: Variable? = null
var isLocked: Boolean = false
abstract fun lockVar(depth: Int)
abstract fun unlockVar(depth: Int)
}

View File

@ -42,4 +42,22 @@ data class VimDictionary(val dictionary: LinkedHashMap<VimString, VimDataType>)
this
}
}
override fun lockVar(depth: Int) {
this.isLocked = true
if (depth > 1) {
for (value in dictionary.values) {
value.lockVar(depth - 1)
}
}
}
override fun unlockVar(depth: Int) {
this.isLocked = false
if (depth > 1) {
for (value in dictionary.values) {
value.unlockVar(depth - 1)
}
}
}
}

View File

@ -26,4 +26,12 @@ data class VimFloat(val value: Double) : VimDataType() {
override fun deepCopy(level: Int): VimFloat {
return copy()
}
override fun lockVar(depth: Int) {
this.isLocked = true
}
override fun unlockVar(depth: Int) {
this.isLocked = false
}
}

View File

@ -108,6 +108,14 @@ data class VimFuncref(
return copy()
}
override fun lockVar(depth: Int) {
this.isLocked = true
}
override fun unlockVar(depth: Int) {
this.isLocked = false
}
enum class Type {
LAMBDA,
FUNCREF,

View File

@ -22,6 +22,14 @@ data class VimInt(val value: Int) : VimDataType() {
return copy()
}
override fun lockVar(depth: Int) {
this.isLocked = true
}
override fun unlockVar(depth: Int) {
this.isLocked = false
}
operator fun compareTo(b: Int): Int = this.value.compareTo(b)
companion object {

View File

@ -36,4 +36,22 @@ data class VimList(val values: MutableList<VimDataType>) : VimDataType() {
this
}
}
override fun lockVar(depth: Int) {
this.isLocked = true
if (depth > 1) {
for (value in values) {
value.lockVar(depth - 1)
}
}
}
override fun unlockVar(depth: Int) {
this.isLocked = false
if (depth > 1) {
for (value in values) {
value.unlockVar(depth - 1)
}
}
}
}

View File

@ -65,4 +65,12 @@ data class VimString(val value: String) : VimDataType() {
override fun deepCopy(level: Int): VimString {
return copy()
}
override fun lockVar(depth: Int) {
this.isLocked = true
}
override fun unlockVar(depth: Int) {
this.isLocked = false
}
}

View File

@ -39,6 +39,7 @@ import com.maddyhome.idea.vim.vimscript.model.commands.HistoryCommand
import com.maddyhome.idea.vim.vimscript.model.commands.JoinLinesCommand
import com.maddyhome.idea.vim.vimscript.model.commands.JumpsCommand
import com.maddyhome.idea.vim.vimscript.model.commands.LetCommand
import com.maddyhome.idea.vim.vimscript.model.commands.LockVarCommand
import com.maddyhome.idea.vim.vimscript.model.commands.MarkCommand
import com.maddyhome.idea.vim.vimscript.model.commands.MarksCommand
import com.maddyhome.idea.vim.vimscript.model.commands.MoveTextCommand
@ -74,6 +75,7 @@ import com.maddyhome.idea.vim.vimscript.model.commands.TabCloseCommand
import com.maddyhome.idea.vim.vimscript.model.commands.TabOnlyCommand
import com.maddyhome.idea.vim.vimscript.model.commands.UndoCommand
import com.maddyhome.idea.vim.vimscript.model.commands.UnknownCommand
import com.maddyhome.idea.vim.vimscript.model.commands.UnlockVarCommand
import com.maddyhome.idea.vim.vimscript.model.commands.WriteAllCommand
import com.maddyhome.idea.vim.vimscript.model.commands.WriteCommand
import com.maddyhome.idea.vim.vimscript.model.commands.WriteNextFileCommand
@ -784,6 +786,15 @@ object CommandVisitor : VimscriptBaseVisitor<Command>() {
"ounmap" to UnMapCommand::class,
"iunmap" to UnMapCommand::class,
"cunmap" to UnMapCommand::class,
"lockv" to LockVarCommand::class,
"lockva" to LockVarCommand::class,
"lockvar" to LockVarCommand::class,
"unlo" to UnlockVarCommand::class,
"unloc" to UnlockVarCommand::class,
"unlock" to UnlockVarCommand::class,
"unlockv" to UnlockVarCommand::class,
"unlockva" to UnlockVarCommand::class,
"unlockvar" to UnlockVarCommand::class,
)
private fun getCommandByName(commandName: String): KClass<out Command> {

View File

@ -52,6 +52,21 @@ object VariableService {
}
}
fun isVariableLocked(variable: Variable, editor: Editor, context: DataContext, parent: Executable): Boolean {
return getNullableVariableValue(variable, editor, context, parent)?.isLocked ?: false
}
fun lockVariable(variable: Variable, depth: Int, editor: Editor, context: DataContext, parent: Executable) {
val value = getNullableVariableValue(variable, editor, context, parent) ?: return
value.lockOwner = variable
value.lockVar(depth)
}
fun unlockVariable(variable: Variable, depth: Int, editor: Editor, context: DataContext, parent: Executable) {
val value = getNullableVariableValue(variable, editor, context, parent) ?: return
value.unlockVar(depth)
}
fun storeVariable(variable: Variable, value: VimDataType, editor: Editor, context: DataContext, parent: Executable) {
val scope = variable.scope ?: getDefaultVariableScope(parent)
when (scope) {

View File

@ -0,0 +1,153 @@
/*
* IdeaVim - Vim emulator for IDEs based on the IntelliJ platform
* Copyright (C) 2003-2021 The IdeaVim authors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jetbrains.plugins.ideavim.ex.implementation.commands
import org.jetbrains.plugins.ideavim.SkipNeovimReason
import org.jetbrains.plugins.ideavim.TestWithoutNeovim
import org.jetbrains.plugins.ideavim.VimTestCase
class LockVarCommandTest : VimTestCase() {
@TestWithoutNeovim(SkipNeovimReason.PLUGIN_ERROR)
fun `test lock int variable`() {
configureByText("\n")
typeText(commandToKeys("let x = 10"))
typeText(commandToKeys("lockvar x"))
typeText(commandToKeys("let x = 15"))
assertPluginError(true)
assertPluginErrorMessageContains("E741: Value is locked: x")
}
fun `test unlock int variable`() {
configureByText("\n")
typeText(commandToKeys("let x = 10"))
typeText(commandToKeys("lockvar x"))
typeText(commandToKeys("unlockvar x"))
typeText(commandToKeys("let x = 15"))
assertPluginError(false)
typeText(commandToKeys("echo x"))
assertExOutput("15\n")
}
@TestWithoutNeovim(SkipNeovimReason.PLUGIN_ERROR)
fun `test lock list variable`() {
configureByText("\n")
typeText(commandToKeys("let x = [1]"))
typeText(commandToKeys("lockvar x"))
typeText(commandToKeys("let x = 15"))
assertPluginError(true)
assertPluginErrorMessageContains("E741: Value is locked: x")
}
@TestWithoutNeovim(SkipNeovimReason.PLUGIN_ERROR)
fun `test lock list variable 2`() {
configureByText("\n")
typeText(commandToKeys("let x = [1]"))
typeText(commandToKeys("lockvar x"))
typeText(commandToKeys("let x += [2]"))
assertPluginError(true)
assertPluginErrorMessageContains("E741: Value is locked: x")
}
fun `test reassigning assigned locked value`() {
configureByText("\n")
typeText(commandToKeys("let x = 10"))
typeText(commandToKeys("lockvar x"))
typeText(commandToKeys("let y = x"))
typeText(commandToKeys("echo y"))
assertExOutput("10\n")
typeText(commandToKeys("let y = 15"))
typeText(commandToKeys("echo y"))
assertExOutput("15\n")
assertPluginError(false)
}
@TestWithoutNeovim(SkipNeovimReason.PLUGIN_ERROR)
fun `test list elements are also locked`() {
configureByText("\n")
typeText(commandToKeys("let x = [1, 2]"))
typeText(commandToKeys("lockvar x"))
typeText(commandToKeys("let x[0] = 15"))
assertPluginError(true)
assertPluginErrorMessageContains("E741: Value is locked")
typeText(commandToKeys("echo x"))
assertExOutput("[1, 2]\n")
}
@TestWithoutNeovim(SkipNeovimReason.PLUGIN_ERROR)
fun `test dict elements are also locked`() {
configureByText("\n")
typeText(commandToKeys("let x = {'one': 1}"))
typeText(commandToKeys("lockvar x"))
typeText(commandToKeys("let x.two = 2"))
assertPluginError(true)
assertPluginErrorMessageContains("E741: Value is locked")
typeText(commandToKeys("echo x"))
assertExOutput("{'one': 1}\n")
}
@TestWithoutNeovim(SkipNeovimReason.PLUGIN_ERROR)
fun `test can modify dict elements but not the dict itself`() {
configureByText("\n")
typeText(commandToKeys("let x = {'one': 1}"))
typeText(commandToKeys("lockvar 1 x"))
typeText(commandToKeys("let x.one = 42"))
assertPluginError(false)
typeText(commandToKeys("echo x"))
assertExOutput("{'one': 42}\n")
typeText(commandToKeys("let x.two = 2"))
assertPluginError(true)
assertPluginErrorMessageContains("E741: Value is locked")
typeText(commandToKeys("echo x"))
assertExOutput("{'one': 42}\n")
}
@TestWithoutNeovim(SkipNeovimReason.PLUGIN_ERROR)
fun `test dict elements are also locked 2`() {
configureByText("\n")
typeText(commandToKeys("let x = {'one': 1}"))
typeText(commandToKeys("lockvar x"))
typeText(commandToKeys("let x['two'] = 2"))
assertPluginError(true)
assertPluginErrorMessageContains("E741: Value is locked")
typeText(commandToKeys("echo x"))
assertExOutput("{'one': 1}\n")
}
fun `test default lock depth`() {
configureByText("\n")
typeText(commandToKeys("let x = {'list': [1]}"))
typeText(commandToKeys("lockvar x"))
typeText(commandToKeys("let x.list[0] = 42"))
typeText(commandToKeys("echo x"))
assertExOutput("{'list': [42]}\n")
}
@TestWithoutNeovim(SkipNeovimReason.PLUGIN_ERROR)
fun `test custom lock depth`() {
configureByText("\n")
typeText(commandToKeys("let x = {'list': [1]}"))
typeText(commandToKeys("lockvar 3 x"))
typeText(commandToKeys("let x.list[0] = 42"))
assertPluginError(true)
assertPluginErrorMessageContains("E741: Value is locked")
typeText(commandToKeys("echo x"))
assertExOutput("{'list': [1]}\n")
}
}