mirror of
https://github.com/chylex/IntelliJ-IdeaVim.git
synced 2025-05-15 00:34:06 +02:00
214 lines
6.7 KiB
Kotlin
214 lines
6.7 KiB
Kotlin
/*
|
|
* 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 com.maddyhome.idea.vim.ex.vimscript
|
|
|
|
import com.maddyhome.idea.vim.ex.CommandParser
|
|
import com.maddyhome.idea.vim.ex.ExException
|
|
import com.maddyhome.idea.vim.ui.VimRcFileState
|
|
import org.jetbrains.annotations.NonNls
|
|
import java.io.File
|
|
import java.io.IOException
|
|
import java.nio.file.Paths
|
|
import java.util.regex.Matcher
|
|
import java.util.regex.Pattern
|
|
|
|
/**
|
|
* @author vlan
|
|
*/
|
|
object VimScriptParser {
|
|
// [VERSION UPDATE] 203+ Annotation should be replaced with @NlsSafe
|
|
@NonNls
|
|
private const val VIMRC_FILE_NAME = "ideavimrc"
|
|
|
|
// [VERSION UPDATE] 203+ Annotation should be replaced with @NlsSafe
|
|
@NonNls
|
|
private val HOME_VIMRC_PATHS = arrayOf(".$VIMRC_FILE_NAME", "_$VIMRC_FILE_NAME")
|
|
|
|
// [VERSION UPDATE] 203+ Annotation should be replaced with @NlsSafe
|
|
@NonNls
|
|
private val XDG_VIMRC_PATH = "ideavim" + File.separator + VIMRC_FILE_NAME
|
|
private val DOUBLE_QUOTED_STRING = Pattern.compile("\"([^\"]*)\"")
|
|
private val SINGLE_QUOTED_STRING = Pattern.compile("'([^']*)'")
|
|
private val REFERENCE_EXPR = Pattern.compile("([A-Za-z_][A-Za-z_0-9]*)")
|
|
private val DEC_NUMBER = Pattern.compile("(\\d+)")
|
|
|
|
// This is a pattern used in ideavimrc parsing for a long time. It removes all trailing/leading spaced and blank lines
|
|
private val EOL_SPLIT_PATTERN = Pattern.compile(" *(\r\n|\n)+ *")
|
|
|
|
var executingVimScript = false
|
|
|
|
@JvmStatic
|
|
fun findIdeaVimRc(): File? {
|
|
val homeDirName = System.getProperty("user.home")
|
|
// Check whether file exists in home dir
|
|
if (homeDirName != null) {
|
|
for (fileName in HOME_VIMRC_PATHS) {
|
|
val file = File(homeDirName, fileName)
|
|
if (file.exists()) {
|
|
return file
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check in XDG config directory
|
|
val xdgConfigHomeProperty = System.getenv("XDG_CONFIG_HOME")
|
|
val xdgConfig = if (xdgConfigHomeProperty == null || xdgConfigHomeProperty == "") {
|
|
if (homeDirName != null) Paths.get(homeDirName, ".config", XDG_VIMRC_PATH).toFile() else null
|
|
} else {
|
|
File(xdgConfigHomeProperty, XDG_VIMRC_PATH)
|
|
}
|
|
return if (xdgConfig != null && xdgConfig.exists()) xdgConfig else null
|
|
}
|
|
|
|
private val newIdeaVimRcTemplate = """
|
|
"" Source your .vimrc
|
|
"source ~/.vimrc
|
|
|
|
"" -- Suggested options --
|
|
" Show a few lines of context around the cursor. Note that this makes the
|
|
" text scroll if you mouse-click near the start or end of the window.
|
|
set scrolloff=5
|
|
|
|
" Do incremental searching.
|
|
set incsearch
|
|
|
|
" Don't use Ex mode, use Q for formatting.
|
|
map Q gq
|
|
|
|
|
|
"" -- Map IDE actions to IdeaVim -- https://jb.gg/abva4t
|
|
"" Map \r to the Reformat Code action
|
|
"map \r <Action>(ReformatCode)
|
|
|
|
"" Map <leader>d to start debug
|
|
"map <leader>d <Action>(Debug)
|
|
|
|
"" Map \b to toggle the breakpoint on the current line
|
|
"map \b <Action>(ToggleLineBreakpoint)
|
|
|
|
|
|
" Find more examples here: https://jb.gg/share-ideavimrc
|
|
|
|
""".trimIndent()
|
|
|
|
fun findOrCreateIdeaVimRc(): File? {
|
|
val found = findIdeaVimRc()
|
|
if (found != null) return found
|
|
|
|
val homeDirName = System.getProperty("user.home")
|
|
if (homeDirName != null) {
|
|
for (fileName in HOME_VIMRC_PATHS) {
|
|
try {
|
|
val file = File(homeDirName, fileName)
|
|
file.createNewFile()
|
|
file.writeText(newIdeaVimRcTemplate)
|
|
VimRcFileState.filePath = file.absolutePath
|
|
return file
|
|
} catch (ignored: IOException) {
|
|
// Try to create one of two files
|
|
}
|
|
}
|
|
}
|
|
return null
|
|
}
|
|
|
|
@JvmStatic
|
|
fun executeFile(file: File): List<String> {
|
|
val data = try {
|
|
readFile(file)
|
|
} catch (ignored: IOException) {
|
|
return emptyList()
|
|
}
|
|
executeText(data)
|
|
return data
|
|
}
|
|
|
|
fun executeText(vararg text: String) {
|
|
executeText(listOf(*text))
|
|
}
|
|
|
|
fun executeText(text: List<String>) {
|
|
for (line in text) {
|
|
// TODO: Build a proper parse tree for a VimL file instead of ignoring potentially nested lines (VIM-669)
|
|
if (line.startsWith(" ") || line.startsWith("\t")) continue
|
|
|
|
val lineToExecute = if (line.startsWith(":")) line.substring(1) else line
|
|
try {
|
|
val command = CommandParser.parse(lineToExecute)
|
|
val commandHandler = CommandParser.getCommandHandler(command)
|
|
if (commandHandler is VimScriptCommandHandler) {
|
|
commandHandler.execute(command)
|
|
}
|
|
} catch (ignored: ExException) {
|
|
}
|
|
}
|
|
}
|
|
|
|
@Throws(ExException::class)
|
|
fun evaluate(expression: String, globals: Map<String?, Any?>): Any {
|
|
// This evaluator is very basic, no proper parsing whatsoever. It is here as the very first step necessary to
|
|
// support mapleader, VIM-650. See also VIM-669.
|
|
var m: Matcher = DOUBLE_QUOTED_STRING.matcher(expression)
|
|
if (m.matches()) return m.group(1)
|
|
|
|
m = SINGLE_QUOTED_STRING.matcher(expression)
|
|
if (m.matches()) return m.group(1)
|
|
|
|
m = REFERENCE_EXPR.matcher(expression)
|
|
if (m.matches()) {
|
|
val name = m.group(1)
|
|
val value = globals[name]
|
|
return value ?: throw ExException("Undefined variable: $name")
|
|
}
|
|
|
|
m = DEC_NUMBER.matcher(expression)
|
|
if (m.matches()) return m.group(1).toInt()
|
|
|
|
throw ExException("Invalid expression: $expression")
|
|
}
|
|
|
|
@Throws(ExException::class)
|
|
fun expressionToString(value: Any): String {
|
|
// TODO: Return meaningful value representations
|
|
return when (value) {
|
|
is String -> value
|
|
is Int -> value.toString()
|
|
else -> throw ExException("Cannot convert '$value' to string")
|
|
}
|
|
}
|
|
|
|
@Throws(IOException::class)
|
|
fun readFile(file: File): List<String> {
|
|
val lines = ArrayList<String>()
|
|
file.forEachLine { line -> lineProcessor(line, lines) }
|
|
return lines
|
|
}
|
|
|
|
fun readText(data: CharSequence): List<String> {
|
|
val lines = ArrayList<String>()
|
|
EOL_SPLIT_PATTERN.split(data).forEach { line -> lineProcessor(line, lines) }
|
|
return lines
|
|
}
|
|
|
|
fun lineProcessor(line: String, lines: ArrayList<String>) {
|
|
val trimmedLine = line.trim()
|
|
if (trimmedLine.isBlank()) return
|
|
lines += trimmedLine
|
|
}
|
|
}
|