1
0
mirror of https://github.com/chylex/IntelliJ-IdeaVim.git synced 2026-04-11 18:57:41 +02:00

Compare commits

..

24 Commits

Author SHA1 Message Date
2d79870c0f Set plugin version to chylex-55 2026-04-06 20:43:01 +02:00
551c6286ab Fix Ex commands not working 2026-04-06 20:43:01 +02:00
e17e93d143 Preserve visual mode after executing IDE action 2026-04-06 20:43:01 +02:00
18722c240c Make g0/g^/g$ work with soft wraps 2026-04-06 20:43:01 +02:00
945fdf3fe9 Make gj/gk jump over soft wraps 2026-04-06 20:43:01 +02:00
40e9d6ff7a Make camelCase motions adjust based on direction of visual selection 2026-04-06 20:43:00 +02:00
6bce1110e5 Make search highlights temporary 2026-04-06 20:43:00 +02:00
d8876b525a Do not switch to normal mode after inserting a live template 2026-04-06 20:43:00 +02:00
069815326a Exit insert mode after refactoring 2026-04-06 20:43:00 +02:00
755bf21d35 Add action to run last macro in all opened files 2026-04-06 20:43:00 +02:00
565ce9f9e4 Stop macro execution after a failed search 2026-04-06 20:43:00 +02:00
8cb78e26f8 Revert per-caret registers 2026-04-06 20:43:00 +02:00
430bdc2a82 Apply scrolloff after executing native IDEA actions 2026-04-06 20:43:00 +02:00
fe55e3e6eb Automatically add unambiguous imports after running a macro 2026-04-06 20:43:00 +02:00
b7ac7acaf5 Fix(VIM-3986): Exception when pasting register contents containing new line 2026-04-06 20:43:00 +02:00
8cd0e2c266 Fix(VIM-3179): Respect virtual space below editor (imperfectly) 2026-04-06 20:43:00 +02:00
902c005826 Fix(VIM-3178): Workaround to support "Jump to Source" action mapping 2026-04-06 20:43:00 +02:00
b09ded236f Update search register when using f/t 2026-04-06 20:43:00 +02:00
221b5474c9 Add support for count for visual and line motion surround 2026-04-06 20:43:00 +02:00
2a91e67f39 Fix vim-surround not working with multiple cursors
Fixes multiple cursors with vim-surround commands `cs, ds, S` (but not `ys`).
2026-04-06 20:43:00 +02:00
e2bd6a2828 Fix(VIM-696): Restore visual mode after undo/redo, and disable incompatible actions 2026-04-06 20:43:00 +02:00
52ff8012cc Respect count with <Action> mappings 2026-04-06 20:42:59 +02:00
0bac02e40e Change matchit plugin to use HTML patterns in unrecognized files 2026-04-06 20:42:59 +02:00
33740616da Fix ex command panel causing Undock tool window to hide 2026-04-06 20:42:59 +02:00
22 changed files with 429 additions and 411 deletions

View File

@@ -20,7 +20,7 @@ ideaVersion=2026.1
# Values for type: https://plugins.jetbrains.com/docs/intellij/tools-gradle-intellij-plugin.html#intellij-extension-type # Values for type: https://plugins.jetbrains.com/docs/intellij/tools-gradle-intellij-plugin.html#intellij-extension-type
ideaType=IU ideaType=IU
instrumentPluginCode=true instrumentPluginCode=true
version=chylex-56 version=chylex-55
javaVersion=21 javaVersion=21
remoteRobotVersion=0.11.23 remoteRobotVersion=0.11.23
antlrVersion=4.10.1 antlrVersion=4.10.1

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright 2003-2023 The IdeaVim authors * Copyright 2003-2026 The IdeaVim authors
* *
* Use of this source code is governed by an MIT-style * Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE.txt file or at * license that can be found in the LICENSE.txt file or at
@@ -9,9 +9,7 @@
package com.maddyhome.idea.vim.newapi package com.maddyhome.idea.vim.newapi
import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.project.Project import com.maddyhome.idea.vim.api.MessageType
import com.intellij.openapi.project.ProjectManager
import com.intellij.openapi.wm.WindowManager
import com.maddyhome.idea.vim.api.VimEditor import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.api.VimMessagesBase import com.maddyhome.idea.vim.api.VimMessagesBase
import com.maddyhome.idea.vim.api.globalOptions import com.maddyhome.idea.vim.api.globalOptions
@@ -25,56 +23,43 @@ internal class IjVimMessages : VimMessagesBase() {
private var message: String? = null private var message: String? = null
private var error = false private var error = false
private var lastBeepTimeMillis = 0L private var lastBeepTimeMillis = 0L
private var allowClearStatusBarMessage = true
override fun showStatusBarMessage(editor: VimEditor?, message: String?) { override fun showMessage(editor: VimEditor, message: String?) {
fun setStatusBarMessage(project: Project, message: String?) { showMessageInternal(editor, message, MessageType.STANDARD)
WindowManager.getInstance().getStatusBar(project)?.let { }
it.info = if (message.isNullOrBlank()) "" else "Vim - $message"
}
}
override fun showErrorMessage(editor: VimEditor, message: String?) {
showMessageInternal(editor, message, MessageType.ERROR)
indicateError()
}
private fun showMessageInternal(editor: VimEditor, message: String?, messageType: MessageType) {
this.message = message this.message = message
val project = editor?.ij?.project if (message.isNullOrBlank()) {
if (project != null) { clearStatusBarMessage()
setStatusBarMessage(project, message) return
} else {
// TODO: We really shouldn't set the status bar text for other projects. That's rude.
ProjectManager.getInstance().openProjects.forEach {
setStatusBarMessage(it, message)
}
} }
// Redraw happens automatically based on changes or scrolling. If we've just set the message (e.g., searching for a val context = injector.executionContextManager.getEditorExecutionContext(editor)
// string, hitting the bottom and scrolling to the top), make sure we don't immediately clear it when scrolling. injector.outputPanel.output(editor, context, message, messageType)
allowClearStatusBarMessage = false }
ApplicationManager.getApplication().invokeLater {
allowClearStatusBarMessage = true @Suppress("DEPRECATION")
override fun showStatusBarMessage(editor: VimEditor?, message: String?) {
if (editor != null) {
showMessage(editor, message)
} else {
// Legacy path for when editor is null - just store the message
this.message = message
} }
} }
override fun getStatusBarMessage(): String? = message override fun getStatusBarMessage(): String? = message
// Vim doesn't appear to have a policy about clearing the status bar, other than on "redraw". This can be forced with
// <C-L> or the `:redraw` command, but also happens as the screen changes, e.g., when inserting or deleting lines,
// scrolling, entering Command-line mode and probably lots more. We should manually clear the status bar when these
// things happen.
override fun clearStatusBarMessage() { override fun clearStatusBarMessage() {
val currentMessage = message if (message.isNullOrEmpty()) return
if (currentMessage.isNullOrEmpty()) return injector.outputPanel.getCurrentOutputPanel()?.close()
// Don't clear the status bar message if we've only just set it
if (!allowClearStatusBarMessage) return
ProjectManager.getInstance().openProjects.forEach { project ->
WindowManager.getInstance().getStatusBar(project)?.let { statusBar ->
// Only clear the status bar if it's showing our last message
if (statusBar.info?.contains(currentMessage) == true) {
statusBar.info = ""
}
}
}
message = null message = null
} }

View File

@@ -14,6 +14,7 @@ import com.intellij.openapi.editor.Editor
import com.intellij.openapi.wm.impl.IdeBackgroundUtil import com.intellij.openapi.wm.impl.IdeBackgroundUtil
import com.intellij.openapi.wm.impl.ToolWindowManagerImpl import com.intellij.openapi.wm.impl.ToolWindowManagerImpl
import com.intellij.ui.ClientProperty import com.intellij.ui.ClientProperty
import com.intellij.ui.JBColor
import com.intellij.ui.components.JBPanel import com.intellij.ui.components.JBPanel
import com.intellij.ui.components.JBScrollPane import com.intellij.ui.components.JBScrollPane
import com.intellij.util.IJSwingUtilities import com.intellij.util.IJSwingUtilities
@@ -24,7 +25,6 @@ import com.maddyhome.idea.vim.api.MessageType
import com.maddyhome.idea.vim.api.VimOutputPanel import com.maddyhome.idea.vim.api.VimOutputPanel
import com.maddyhome.idea.vim.api.globalOptions import com.maddyhome.idea.vim.api.globalOptions
import com.maddyhome.idea.vim.api.injector import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.diagnostic.VimLogger
import com.maddyhome.idea.vim.helper.requestFocus import com.maddyhome.idea.vim.helper.requestFocus
import com.maddyhome.idea.vim.helper.selectEditorFont import com.maddyhome.idea.vim.helper.selectEditorFont
import com.maddyhome.idea.vim.helper.vimMorePanel import com.maddyhome.idea.vim.helper.vimMorePanel
@@ -36,121 +36,166 @@ import java.awt.event.ComponentAdapter
import java.awt.event.ComponentEvent import java.awt.event.ComponentEvent
import java.awt.event.KeyAdapter import java.awt.event.KeyAdapter
import java.awt.event.KeyEvent import java.awt.event.KeyEvent
import java.lang.ref.WeakReference
import javax.swing.JComponent import javax.swing.JComponent
import javax.swing.JLabel import javax.swing.JLabel
import javax.swing.JRootPane
import javax.swing.JScrollPane import javax.swing.JScrollPane
import javax.swing.JTextArea import javax.swing.JTextPane
import javax.swing.KeyStroke import javax.swing.KeyStroke
import javax.swing.SwingUtilities import javax.swing.SwingUtilities
import javax.swing.text.DefaultCaret
import javax.swing.text.SimpleAttributeSet
import javax.swing.text.StyleConstants
import javax.swing.text.StyledDocument
import kotlin.math.ceil import kotlin.math.ceil
import kotlin.math.min import kotlin.math.min
/** /**
* This panel displays text in a `more` like window and implements [VimOutputPanel]. * Panel that displays text in a `more` like window overlaid on the editor.
*/ */
class OutputPanel(editorRef: WeakReference<Editor>) : JBPanel<OutputPanel?>(), VimOutputPanel { class OutputPanel private constructor(
private val myEditorRef: WeakReference<Editor> = editorRef private val editor: Editor,
val editor: Editor? get() = myEditorRef.get() ) : JBPanel<OutputPanel>(), VimOutputPanel {
val myLabel: JLabel = JLabel("more") private val textPane = JTextPane()
private val myText = JTextArea() private val resizeAdapter: ComponentAdapter
private val myScrollPane: JScrollPane = private var defaultForeground: Color? = null
JBScrollPane(myText, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER)
private val myAdapter: ComponentAdapter
private var myLineHeight = 0
private var myOldGlass: JComponent? = null private var glassPane: JComponent? = null
private var myOldLayout: LayoutManager? = null private var originalLayout: LayoutManager? = null
private var myWasOpaque = false private var wasOpaque = false
var myActive: Boolean = false var active: Boolean = false
private val segments = mutableListOf<TextLine>()
val isActive: Boolean private val labelComponent: JLabel = JLabel("more")
get() = myActive private val scrollPane: JScrollPane =
JBScrollPane(textPane, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER)
private var cachedLineHeight = 0
private var isSingleLine = false
init { init {
// Create a text editor for the text and a label for the prompt textPane.isEditable = false
val layout = BorderLayout(0, 0) textPane.caret = object : DefaultCaret() {
setLayout(layout) override fun setVisible(v: Boolean) {
add(myScrollPane, BorderLayout.CENTER) super.setVisible(false)
add(myLabel, BorderLayout.SOUTH) }
}
textPane.highlighter = null
// Set the text area read only, and support wrap resizeAdapter = object : ComponentAdapter() {
myText.isEditable = false
myText.setLineWrap(true)
myAdapter = object : ComponentAdapter() {
override fun componentResized(e: ComponentEvent?) { override fun componentResized(e: ComponentEvent?) {
positionPanel() positionPanel()
} }
} }
// Setup some listeners to handle keystrokes // Suppress the fancy frame background used in the Islands theme
val moreKeyListener = MoreKeyListener() ClientProperty.putRecursive(this, IdeBackgroundUtil.NO_BACKGROUND, true)
addKeyListener(moreKeyListener) putClientProperty(ToolWindowManagerImpl.PARENT_COMPONENT, editor.component)
myText.addKeyListener(moreKeyListener)
// Suppress the fancy frame background used in the Islands theme, which comes from a custom Graphics implementation // Initialize panel
// applied to the IdeRoot, and used to paint all children, including this panel. This client property is checked by setLayout(BorderLayout(0, 0))
// JBPanel.getComponentGraphics to give us the original Graphics, opting out of the fancy painting. add(scrollPane, BorderLayout.CENTER)
ClientProperty.putRecursive<Boolean?>(this, IdeBackgroundUtil.NO_BACKGROUND, true) add(labelComponent, BorderLayout.SOUTH)
editor?.let { putClientProperty(ToolWindowManagerImpl.PARENT_COMPONENT, it.component) }
val keyListener = OutputPanelKeyListener()
addKeyListener(keyListener)
textPane.addKeyListener(keyListener)
updateUI() updateUI()
} }
// Called automatically when the LAF is changed and the component is visible, and manually by the LAF listener handler
override fun updateUI() { override fun updateUI() {
super.updateUI() super.updateUI()
setBorder(ExPanelBorder()) setBorder(ExPanelBorder())
// Swing uses a bad pattern of calling updateUI() from the constructor. At this moment, all these variables are null
@Suppress("SENSELESS_COMPARISON") @Suppress("SENSELESS_COMPARISON")
if (myText != null && myLabel != null && myScrollPane != null) { if (textPane != null && labelComponent != null && scrollPane != null) {
setFontForElements() setFontForElements()
myText.setBorder(null) textPane.setBorder(null)
myScrollPane.setBorder(null) scrollPane.setBorder(null)
myLabel.setForeground(myText.getForeground()) labelComponent.setForeground(textPane.getForeground())
// Make sure the panel is positioned correctly in case we're changing font size
positionPanel() positionPanel()
} }
} }
override var text: String override var text: String
get() = myText.text get() = textPane.getText() ?: ""
set(value) { set(value) {
// ExOutputPanel will strip a trailing newline. We'll do it now so that tests have the same behaviour.
val newValue = value.removeSuffix("\n") val newValue = value.removeSuffix("\n")
myText.text = newValue segments.clear()
val ed = editor if (newValue.isEmpty()) return
if (ed != null) { segments.add(TextLine(newValue, null))
myText.setFont(selectEditorFont(ed, newValue))
}
myText.setCaretPosition(0)
if (newValue.isNotEmpty()) {
activate()
}
} }
override var label: String override var label: String
get() = myLabel.text ?: "" get() = labelComponent.text
set(value) { set(value) {
myLabel.text = value labelComponent.text = value
val ed = editor
if (ed != null) {
myLabel.setFont(selectEditorFont(ed, value))
}
} }
override fun addText(text: String, isNewLine: Boolean, messageType: MessageType) { /**
if (this.text.isNotEmpty() && isNewLine) { * Sets styled text with multiple segments, each potentially having a different color.
this.text += "\n$text" */
fun setStyledText(lines: List<TextLine>) {
val doc = textPane.styledDocument
doc.remove(0, doc.length)
if (defaultForeground == null) {
defaultForeground = textPane.foreground
}
if (lines.size > 1) {
setMultiLineText(lines, doc)
} else { } else {
this.text += text doc.insertString(doc.length, lines[0].text.removeSuffix("\n"), getLineColor(lines[0]))
}
val fullText = doc.getText(0, doc.length)
textPane.setFont(selectEditorFont(editor, fullText))
textPane.setCaretPosition(0)
if (fullText.isNotEmpty()) {
activate()
}
}
private fun setMultiLineText(
lines: List<TextLine>,
doc: StyledDocument,
) {
for ((index, line) in lines.withIndex()) {
val text = line.text.removeSuffix("\n")
val attrs = getLineColor(line)
val separator = if (index < lines.size - 1) "\n" else ""
doc.insertString(doc.length, text + separator, attrs)
}
}
private fun getLineColor(segment: TextLine): SimpleAttributeSet {
val attrs = SimpleAttributeSet()
val color = segment.color ?: defaultForeground
if (color != null) {
StyleConstants.setForeground(attrs, color)
}
return attrs
}
override fun addText(text: String, isNewLine: Boolean, messageType: MessageType) {
val color = when (messageType) {
MessageType.ERROR -> JBColor.RED
MessageType.STANDARD -> null
}
segments.add(TextLine(text, color))
}
override fun show() {
val currentPanel = injector.outputPanel.getCurrentOutputPanel()
if (currentPanel != null && currentPanel != this) currentPanel.close()
setStyledText(segments)
if (!active) {
activate()
} }
} }
@@ -159,20 +204,15 @@ class OutputPanel(editorRef: WeakReference<Editor>) : JBPanel<OutputPanel?>(), V
} }
override fun clearText() { override fun clearText() {
segments.clear()
}
fun clear() {
text = "" text = ""
} }
override fun show() {
editor ?: return
val currentPanel = injector.outputPanel.getCurrentOutputPanel()
if (currentPanel != null && currentPanel != this) currentPanel.close()
if (!myActive) {
activate()
}
}
override fun handleKey(key: KeyStroke) { override fun handleKey(key: KeyStroke) {
if (isAtEnd) { if (isAtEnd) {
close(key) close(key)
return return
@@ -197,214 +237,262 @@ class OutputPanel(editorRef: WeakReference<Editor>) : JBPanel<OutputPanel?>(), V
override fun getForeground(): Color? { override fun getForeground(): Color? {
@Suppress("SENSELESS_COMPARISON") @Suppress("SENSELESS_COMPARISON")
if (myText == null) { if (textPane == null) {
// Swing uses a bad pattern of calling getForeground() from the constructor. At this moment, `myText` is null.
return super.getForeground() return super.getForeground()
} }
return myText.getForeground() return textPane.getForeground()
} }
override fun getBackground(): Color? { override fun getBackground(): Color? {
@Suppress("SENSELESS_COMPARISON") @Suppress("SENSELESS_COMPARISON")
if (myText == null) { if (textPane == null) {
// Swing uses a bad pattern of calling getBackground() from the constructor. At this moment, `myText` is null.
return super.getBackground() return super.getBackground()
} }
return myText.getBackground() return textPane.getBackground()
} }
/** /**
* Turns off the ex entry field and optionally puts the focus back to the original component * Turns off the output panel and optionally puts the focus back to the original component.
*/ */
fun deactivate(refocusOwningEditor: Boolean) { fun deactivate(refocusOwningEditor: Boolean) {
if (!myActive) return if (!active) return
myActive = false active = false
myText.text = "" clearText()
val ed = editor textPane.text = ""
if (refocusOwningEditor && ed != null) { if (refocusOwningEditor) {
requestFocus(ed.contentComponent) requestFocus(editor.contentComponent)
} }
if (myOldGlass != null) { if (glassPane != null) {
myOldGlass!!.removeComponentListener(myAdapter) glassPane!!.removeComponentListener(resizeAdapter)
myOldGlass!!.isVisible = false glassPane!!.isVisible = false
myOldGlass!!.remove(this) glassPane!!.remove(this)
myOldGlass!!.setOpaque(myWasOpaque) glassPane!!.setOpaque(wasOpaque)
myOldGlass!!.setLayout(myOldLayout) glassPane!!.setLayout(originalLayout)
} }
} }
/** /**
* Turns on the more window for the given editor * Turns on the output panel for the given editor.
*/ */
fun activate() { fun activate() {
val ed = editor ?: return disableOldGlass()
val root = SwingUtilities.getRootPane(ed.contentComponent)
deactivateOldGlass(root)
setFontForElements() setFontForElements()
positionPanel() positionPanel()
if (myOldGlass != null) { if (glassPane != null) {
myOldGlass!!.isVisible = true glassPane!!.isVisible = true
} }
myActive = true active = true
requestFocus(myText) requestFocus(textPane)
} }
private fun deactivateOldGlass(root: JRootPane?) { private fun disableOldGlass() {
if (root == null) return val root = SwingUtilities.getRootPane(editor.contentComponent) ?: return
myOldGlass = root.getGlassPane() as JComponent? glassPane = root.getGlassPane() as JComponent?
if (myOldGlass != null) { if (glassPane == null) {
myOldLayout = myOldGlass!!.layout
myWasOpaque = myOldGlass!!.isOpaque
myOldGlass!!.setLayout(null)
myOldGlass!!.setOpaque(false)
myOldGlass!!.add(this)
myOldGlass!!.addComponentListener(myAdapter)
}
}
private fun setFontForElements() {
val ed = editor ?: return
myText.setFont(selectEditorFont(ed, myText.getText()))
myLabel.setFont(selectEditorFont(ed, myLabel.text))
}
override fun scrollLine() {
scrollOffset(myLineHeight)
}
override fun scrollPage() {
scrollOffset(myScrollPane.getVerticalScrollBar().visibleAmount)
}
override fun scrollHalfPage() {
val sa = myScrollPane.getVerticalScrollBar().visibleAmount / 2.0
val offset = ceil(sa / myLineHeight) * myLineHeight
scrollOffset(offset.toInt())
}
fun onBadKey() {
val ed = editor ?: return
myLabel.setText(injector.messages.message("message.ex.output.more.prompt.full"))
myLabel.setFont(selectEditorFont(ed, myLabel.text))
}
private fun scrollOffset(more: Int) {
val ed = editor ?: return
val `val` = myScrollPane.getVerticalScrollBar().value
myScrollPane.getVerticalScrollBar().setValue(`val` + more)
myScrollPane.getHorizontalScrollBar().setValue(0)
if (isAtEnd) {
myLabel.setText(injector.messages.message("message.ex.output.end.prompt"))
} else {
myLabel.setText(injector.messages.message("message.ex.output.more.prompt"))
}
myLabel.setFont(selectEditorFont(ed, myLabel.text))
}
val isAtEnd: Boolean
get() {
val isSingleLine = myText.getLineCount() == 1
if (isSingleLine) return true
val scrollBar = myScrollPane.getVerticalScrollBar()
val value = scrollBar.value
if (!scrollBar.isVisible) {
return true
}
return value >= scrollBar.maximum - scrollBar.visibleAmount ||
scrollBar.maximum <= scrollBar.visibleAmount
}
private fun positionPanel() {
val ed = editor ?: return
val contentComponent = ed.contentComponent
val scroll = SwingUtilities.getAncestorOfClass(JScrollPane::class.java, contentComponent)
val rootPane = SwingUtilities.getRootPane(contentComponent)
if (scroll == null || rootPane == null) {
// These might be null if we're invoked during component initialisation and before it's been added to the tree
return return
} }
originalLayout = glassPane!!.layout
size = scroll.size wasOpaque = glassPane!!.isOpaque
glassPane!!.setLayout(null)
myLineHeight = myText.getFontMetrics(myText.getFont()).height glassPane!!.setOpaque(false)
val count: Int = countLines(myText.getText()) glassPane!!.add(this)
val visLines = size.height / myLineHeight - 1 glassPane!!.addComponentListener(resizeAdapter)
val lines = min(count, visLines)
setSize(
size.width,
lines * myLineHeight + myLabel.getPreferredSize().height + border.getBorderInsets(this).top * 2
)
val height = size.height
val bounds = scroll.bounds
bounds.translate(0, scroll.getHeight() - height)
bounds.height = height
val pos = SwingUtilities.convertPoint(scroll.getParent(), bounds.location, rootPane.getGlassPane())
bounds.location = pos
setBounds(bounds)
myScrollPane.getVerticalScrollBar().setValue(0)
if (!injector.globalOptions().more) {
// FIX
scrollOffset(100000)
} else {
scrollOffset(0)
}
}
fun close(key: KeyStroke? = null) {
val ed = editor ?: return
ApplicationManager.getApplication().invokeLater {
deactivate(true)
val project = ed.project
if (project != null && key != null && key.keyChar != '\n') {
val keys: MutableList<KeyStroke> = ArrayList(1)
keys.add(key)
if (LOG.isTrace()) {
LOG.trace(
"Adding new keys to keyStack as part of playback. State before adding keys: " +
getInstance().keyStack.dump()
)
}
getInstance().keyStack.addKeys(keys)
val context: ExecutionContext =
injector.executionContextManager.getEditorExecutionContext(IjVimEditor(ed))
VimPlugin.getMacro().playbackKeys(IjVimEditor(ed), context, 1)
}
}
} }
override fun close() { override fun close() {
close(null) close(null)
} }
private class MoreKeyListener : KeyAdapter() { fun close(key: KeyStroke?) {
/** val passKeyBack = isSingleLine
* Invoked when a key has been pressed. ApplicationManager.getApplication().invokeLater {
*/ deactivate(true)
val project = editor.project
// For single line messages, pass any key back to the editor (including Enter)
// For multi-line messages, don't pass Enter back (it was used to dismiss)
if (project != null && key != null && (passKeyBack || key.keyChar != '\n')) {
val keys: MutableList<KeyStroke> = ArrayList(1)
keys.add(key)
getInstance().keyStack.addKeys(keys)
val context: ExecutionContext =
injector.executionContextManager.getEditorExecutionContext(IjVimEditor(editor))
VimPlugin.getMacro().playbackKeys(IjVimEditor(editor), context, 1)
}
}
}
private fun setFontForElements() {
textPane.setFont(selectEditorFont(editor, textPane.getText()))
labelComponent.setFont(selectEditorFont(editor, labelComponent.text))
}
private fun positionPanel() {
val scroll = positionPanelStart() ?: return
val lineHeight = textPane.getFontMetrics(textPane.getFont()).height
val count = countLines(textPane.getText())
val visLines = size.height / lineHeight - 1
val lines = min(count, visLines)
// Simple output: single line that fits entirely - no label needed
isSingleLine = count == 1 && count <= visLines
labelComponent.isVisible = !isSingleLine
val extraHeight = if (isSingleLine) 0 else labelComponent.getPreferredSize().height
setSize(
size.width,
lines * lineHeight + extraHeight + border.getBorderInsets(this).top * 2
)
finishPositioning(scroll)
// Force layout so that viewport sizes are valid before checking scroll state
validate()
// onPositioned
cachedLineHeight = lineHeight
scrollPane.getVerticalScrollBar().setValue(0)
if (!isSingleLine) {
if (!injector.globalOptions().more) {
scrollOffset(100000)
} else {
scrollOffset(0)
}
}
}
private fun positionPanelStart(): JScrollPane? {
val contentComponent = editor.contentComponent
val scroll = SwingUtilities.getAncestorOfClass(JScrollPane::class.java, contentComponent) as? JScrollPane
val rootPane = SwingUtilities.getRootPane(contentComponent)
if (scroll == null || rootPane == null) {
return null
}
size = scroll.size
return scroll
}
private fun finishPositioning(scroll: JScrollPane) {
val rootPane = SwingUtilities.getRootPane(editor.contentComponent)
val bounds = scroll.bounds
bounds.translate(0, scroll.getHeight() - size.height)
bounds.height = size.height
val pos = SwingUtilities.convertPoint(scroll.getParent(), bounds.location, rootPane.getGlassPane())
bounds.location = pos
setBounds(bounds)
}
private fun countLines(text: String): Int {
if (text.isEmpty()) {
return 1
}
var count = 0
var pos = -1
while ((text.indexOf('\n', pos + 1).also { pos = it }) != -1) {
count++
}
if (text[text.length - 1] != '\n') {
count++
}
return count
}
override fun scrollLine() {
scrollOffset(cachedLineHeight)
}
override fun scrollPage() {
scrollOffset(scrollPane.getVerticalScrollBar().visibleAmount)
}
override fun scrollHalfPage() {
val sa = scrollPane.getVerticalScrollBar().visibleAmount / 2.0
val offset = ceil(sa / cachedLineHeight) * cachedLineHeight
scrollOffset(offset.toInt())
}
fun onBadKey() {
labelComponent.setText(injector.messages.message("message.ex.output.more.prompt.full"))
labelComponent.setFont(selectEditorFont(editor, labelComponent.text))
}
private fun scrollOffset(more: Int) {
scrollPane.validate()
val scrollBar = scrollPane.getVerticalScrollBar()
val value = scrollBar.value
scrollBar.setValue(value + more)
scrollPane.getHorizontalScrollBar().setValue(0)
// Check if we're at the end or if content fits entirely (nothing to scroll)
if (isAtEnd) {
labelComponent.setText(injector.messages.message("message.ex.output.end.prompt"))
} else {
labelComponent.setText(injector.messages.message("message.ex.output.more.prompt"))
}
labelComponent.setFont(selectEditorFont(editor, labelComponent.text))
}
val isAtEnd: Boolean
get() {
if (isSingleLine) return true
val contentHeight = textPane.preferredSize.height
val viewportHeight = scrollPane.viewport.height
if (contentHeight <= viewportHeight) return true
val scrollBar = scrollPane.getVerticalScrollBar()
return scrollBar.value >= scrollBar.maximum - scrollBar.visibleAmount
}
private inner class OutputPanelKeyListener : KeyAdapter() {
override fun keyTyped(e: KeyEvent) { override fun keyTyped(e: KeyEvent) {
val currentPanel: VimOutputPanel = injector.outputPanel.getCurrentOutputPanel() ?: return val currentPanel: VimOutputPanel = injector.outputPanel.getCurrentOutputPanel() ?: return
val keyCode = e.getKeyCode() val keyChar = e.keyChar
val keyChar = e.getKeyChar()
val modifiers = e.modifiersEx val modifiers = e.modifiersEx
val keyStroke = if (keyChar == KeyEvent.CHAR_UNDEFINED) val keyStroke = KeyStroke.getKeyStroke(keyChar, modifiers)
KeyStroke.getKeyStroke(keyCode, modifiers)
else
KeyStroke.getKeyStroke(keyChar, modifiers)
currentPanel.handleKey(keyStroke) currentPanel.handleKey(keyStroke)
} }
override fun keyPressed(e: KeyEvent) {
if (!e.isActionKey && e.keyCode != KeyEvent.VK_ENTER) return
val currentPanel = injector.outputPanel.getCurrentOutputPanel() as? OutputPanel ?: return
val keyCode = e.keyCode
val modifiers = e.modifiersEx
val keyStroke = KeyStroke.getKeyStroke(keyCode, modifiers)
if (isSingleLine) {
currentPanel.close(keyStroke)
e.consume()
return
}
// Multi-line mode: arrow keys scroll, down/right at end closes
when (keyCode) {
KeyEvent.VK_ENTER -> {
if (currentPanel.isAtEnd) currentPanel.close() else currentPanel.scrollLine()
e.consume()
}
KeyEvent.VK_DOWN -> if (currentPanel.isAtEnd) currentPanel.close(keyStroke) else currentPanel.scrollLine()
KeyEvent.VK_RIGHT -> if (currentPanel.isAtEnd) currentPanel.close(keyStroke) else currentPanel.scrollLine()
KeyEvent.VK_UP -> currentPanel.scrollOffset(-cachedLineHeight)
KeyEvent.VK_LEFT -> currentPanel.scrollOffset(-cachedLineHeight)
KeyEvent.VK_PAGE_DOWN -> if (currentPanel.isAtEnd) currentPanel.close(keyStroke) else currentPanel.scrollPage()
KeyEvent.VK_PAGE_UP -> currentPanel.scrollOffset(-scrollPane.verticalScrollBar.visibleAmount)
}
}
} }
class LafListener : LafManagerListener { class LafListener : LafManagerListener {
override fun lookAndFeelChanged(source: LafManager) { override fun lookAndFeelChanged(source: LafManager) {
if (VimPlugin.isNotEnabled()) return if (VimPlugin.isNotEnabled()) return
// This listener is only invoked for local scenarios, and we only need to update local editor UI. This will invoke
// updateUI on the output pane and it's child components
for (vimEditor in injector.editorGroup.getEditors()) { for (vimEditor in injector.editorGroup.getEditors()) {
val editor = (vimEditor as IjVimEditor).editor val editor = (vimEditor as IjVimEditor).editor
if (!isPanelActive(editor)) continue if (!isPanelActive(editor)) continue
@@ -414,41 +502,24 @@ class OutputPanel(editorRef: WeakReference<Editor>) : JBPanel<OutputPanel?>(), V
} }
companion object { companion object {
private val LOG: VimLogger = injector.getLogger<OutputPanel>(OutputPanel::class.java)
fun getNullablePanel(editor: Editor): OutputPanel? { fun getNullablePanel(editor: Editor): OutputPanel? {
return editor.vimMorePanel as? OutputPanel return editor.vimMorePanel as OutputPanel?
} }
fun isPanelActive(editor: Editor): Boolean { fun isPanelActive(editor: Editor): Boolean {
return getNullablePanel(editor)?.myActive ?: false return getNullablePanel(editor) != null
} }
fun getInstance(editor: Editor): OutputPanel { fun getInstance(editor: Editor): OutputPanel {
var panel: OutputPanel? = getNullablePanel(editor) var panel: OutputPanel? = getNullablePanel(editor)
if (panel == null) { if (panel == null) {
panel = OutputPanel(WeakReference(editor)) panel = OutputPanel(editor)
editor.vimMorePanel = panel editor.vimMorePanel = panel
} }
return panel return panel
} }
private fun countLines(text: String): Int {
if (text.isEmpty()) {
return 0
}
var count = 0
var pos = -1
while ((text.indexOf('\n', pos + 1).also { pos = it }) != -1) {
count++
}
if (text[text.length - 1] != '\n') {
count++
}
return count
}
} }
} }
data class TextLine(val text: String, val color: Color?)

View File

@@ -22,11 +22,11 @@ class IjOutputPanelService : VimOutputPanelServiceBase() {
private var activeOutputPanel: WeakReference<VimOutputPanel>? = null private var activeOutputPanel: WeakReference<VimOutputPanel>? = null
override fun getCurrentOutputPanel(): VimOutputPanel? { override fun getCurrentOutputPanel(): VimOutputPanel? {
return activeOutputPanel?.get()?.takeIf { (it as OutputPanel).isActive } return activeOutputPanel?.get()?.takeIf { (it as OutputPanel).active }
} }
override fun create(editor: VimEditor, context: ExecutionContext): VimOutputPanel { override fun create(editor: VimEditor, context: ExecutionContext): VimOutputPanel {
val panel = OutputPanel(WeakReference(editor.ij)) val panel = OutputPanel.getInstance(editor.ij)
activeOutputPanel = WeakReference(panel) activeOutputPanel = WeakReference(panel)
return panel return panel
} }

View File

@@ -1,43 +0,0 @@
package com.maddyhome.idea.vim.vimscript.model.functions.handlers
import com.intellij.openapi.actionSystem.ActionManager
import com.intellij.openapi.actionSystem.impl.PresentationFactory
import com.intellij.openapi.actionSystem.impl.Utils
import com.intellij.openapi.keymap.impl.ActionProcessor
import com.intellij.vim.annotations.VimscriptFunction
import com.maddyhome.idea.vim.api.ExecutionContext
import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.newapi.ij
import com.maddyhome.idea.vim.vimscript.model.VimLContext
import com.maddyhome.idea.vim.vimscript.model.datatypes.VimInt
import com.maddyhome.idea.vim.vimscript.model.datatypes.asVimInt
import com.maddyhome.idea.vim.vimscript.model.functions.BuiltinFunctionHandler
import java.awt.event.KeyEvent
@VimscriptFunction(name = "isactionenabled")
internal class IsActionEnabled : BuiltinFunctionHandler<VimInt>() {
override fun doFunction(
arguments: Arguments,
editor: VimEditor,
context: ExecutionContext,
vimContext: VimLContext,
): VimInt {
val action = ActionManager.getInstance().getAction(arguments.getString(0).value)
if (action == null) {
return false.asVimInt()
}
val presentationFactory = PresentationFactory()
val wrappedContext = Utils.createAsyncDataContext(context.ij)
val actionProcessor = object : ActionProcessor() {}
val inputEventAdjusted = KeyEvent(editor.ij.contentComponent, KeyEvent.KEY_PRESSED, 0L, 0, KeyEvent.VK_UNDEFINED, '\u0000')
val updateEvent = Utils.runUpdateSessionForInputEvent(listOf(action), inputEventAdjusted, wrappedContext, "IdeaVim", actionProcessor, presentationFactory) { _, updater, events ->
val presentation = updater(action)
events[presentation]
}
val result = updateEvent != null && updateEvent.presentation.isEnabled
return result.asVimInt()
}
}

View File

@@ -1,5 +1,4 @@
{ {
"has": "com.maddyhome.idea.vim.vimscript.model.functions.handlers.HasFunctionHandler", "has": "com.maddyhome.idea.vim.vimscript.model.functions.handlers.HasFunctionHandler",
"isactionenabled": "com.maddyhome.idea.vim.vimscript.model.functions.handlers.IsActionEnabled",
"pumvisible": "com.maddyhome.idea.vim.vimscript.model.functions.handlers.PopupMenuVisibleFunctionHandler" "pumvisible": "com.maddyhome.idea.vim.vimscript.model.functions.handlers.PopupMenuVisibleFunctionHandler"
} }

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright 2003-2023 The IdeaVim authors * Copyright 2003-2026 The IdeaVim authors
* *
* Use of this source code is governed by an MIT-style * Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE.txt file or at * license that can be found in the LICENSE.txt file or at
@@ -134,7 +134,7 @@ class CmdCommandTest : VimTestCase() {
VimPlugin.getCommand().resetAliases() VimPlugin.getCommand().resetAliases()
configureByText("\n") configureByText("\n")
typeText(commandToKeys("command! -range Error echo <args>")) typeText(commandToKeys("command! -range Error echo <args>"))
assertPluginError(false) assertPluginError(true)
kotlin.test.assertEquals("'-range' is not supported by `command`", injector.messages.getStatusBarMessage()) kotlin.test.assertEquals("'-range' is not supported by `command`", injector.messages.getStatusBarMessage())
} }
@@ -143,7 +143,7 @@ class CmdCommandTest : VimTestCase() {
VimPlugin.getCommand().resetAliases() VimPlugin.getCommand().resetAliases()
configureByText("\n") configureByText("\n")
typeText(commandToKeys("command! -complete=color Error echo <args>")) typeText(commandToKeys("command! -complete=color Error echo <args>"))
assertPluginError(false) assertPluginError(true)
kotlin.test.assertEquals("'-complete' is not supported by `command`", injector.messages.getStatusBarMessage()) kotlin.test.assertEquals("'-complete' is not supported by `command`", injector.messages.getStatusBarMessage())
} }

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright 2003-2023 The IdeaVim authors * Copyright 2003-2026 The IdeaVim authors
* *
* Use of this source code is governed by an MIT-style * Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE.txt file or at * license that can be found in the LICENSE.txt file or at
@@ -24,7 +24,6 @@ class ExecuteCommandTest : VimTestCase() {
fun `test execute with range`() { fun `test execute with range`() {
configureByText("\n") configureByText("\n")
typeText(commandToKeys("1,2execute 'echo 42'")) typeText(commandToKeys("1,2execute 'echo 42'"))
assertNoExOutput()
assertPluginError(true) assertPluginError(true)
} }

View File

@@ -72,7 +72,6 @@ class HistoryCommandTest : VimTestCase() {
fun `test history with 'history' option set to 0 shows nothing`() { fun `test history with 'history' option set to 0 shows nothing`() {
enterCommand("set history=0") enterCommand("set history=0")
enterCommand("history") enterCommand("history")
assertNoExOutput()
assertPluginError(false) assertPluginError(false)
assertPluginErrorMessage("'history' option is zero") assertPluginErrorMessage("'history' option is zero")
} }

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright 2003-2025 The IdeaVim authors * Copyright 2003-2026 The IdeaVim authors
* *
* Use of this source code is governed by an MIT-style * Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE.txt file or at * license that can be found in the LICENSE.txt file or at
@@ -43,7 +43,6 @@ class AndFunctionTest : VimTestCase() {
@Test @Test
fun `test and function with list causes error`() { fun `test and function with list causes error`() {
enterCommand("echo and([1, 2, 3], [2, 3, 4])") enterCommand("echo and([1, 2, 3], [2, 3, 4])")
assertNoExOutput()
assertPluginError(true) assertPluginError(true)
assertPluginErrorMessage("E745: Using a List as a Number") assertPluginErrorMessage("E745: Using a List as a Number")
} }
@@ -51,7 +50,6 @@ class AndFunctionTest : VimTestCase() {
@Test @Test
fun `test and function with dict causes error`() { fun `test and function with dict causes error`() {
enterCommand("echo and({1: 2, 3: 4}, {3: 4, 5: 6})") enterCommand("echo and({1: 2, 3: 4}, {3: 4, 5: 6})")
assertNoExOutput()
assertPluginError(true) assertPluginError(true)
assertPluginErrorMessage("E728: Using a Dictionary as a Number") assertPluginErrorMessage("E728: Using a Dictionary as a Number")
} }
@@ -59,7 +57,6 @@ class AndFunctionTest : VimTestCase() {
@Test @Test
fun `test and function with float causes error`() { fun `test and function with float causes error`() {
enterCommand("echo and(1.5, 2.5)") enterCommand("echo and(1.5, 2.5)")
assertNoExOutput()
assertPluginError(true) assertPluginError(true)
assertPluginErrorMessage("E805: Using a Float as a Number") assertPluginErrorMessage("E805: Using a Float as a Number")
} }

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright 2003-2025 The IdeaVim authors * Copyright 2003-2026 The IdeaVim authors
* *
* Use of this source code is governed by an MIT-style * Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE.txt file or at * license that can be found in the LICENSE.txt file or at
@@ -28,7 +28,6 @@ class InvertFunctionTest : VimTestCase() {
@Test @Test
fun `test invert function with list causes error`() { fun `test invert function with list causes error`() {
enterCommand("echo invert([1, 2, 3])") enterCommand("echo invert([1, 2, 3])")
assertNoExOutput()
assertPluginError(true) assertPluginError(true)
assertPluginErrorMessage("E745: Using a List as a Number") assertPluginErrorMessage("E745: Using a List as a Number")
} }
@@ -36,7 +35,6 @@ class InvertFunctionTest : VimTestCase() {
@Test @Test
fun `test invert function with dict causes error`() { fun `test invert function with dict causes error`() {
enterCommand("echo invert({1: 2, 3: 4})") enterCommand("echo invert({1: 2, 3: 4})")
assertNoExOutput()
assertPluginError(true) assertPluginError(true)
assertPluginErrorMessage("E728: Using a Dictionary as a Number") assertPluginErrorMessage("E728: Using a Dictionary as a Number")
} }
@@ -44,7 +42,6 @@ class InvertFunctionTest : VimTestCase() {
@Test @Test
fun `test invert function with float causes error`() { fun `test invert function with float causes error`() {
enterCommand("echo invert(1.5)") enterCommand("echo invert(1.5)")
assertNoExOutput()
assertPluginError(true) assertPluginError(true)
assertPluginErrorMessage("E805: Using a Float as a Number") assertPluginErrorMessage("E805: Using a Float as a Number")
} }

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright 2003-2025 The IdeaVim authors * Copyright 2003-2026 The IdeaVim authors
* *
* Use of this source code is governed by an MIT-style * Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE.txt file or at * license that can be found in the LICENSE.txt file or at
@@ -43,7 +43,6 @@ class OrFunctionTest : VimTestCase() {
@Test @Test
fun `test or function with list causes error`() { fun `test or function with list causes error`() {
enterCommand("echo or([1, 2, 3], [2, 3, 4])") enterCommand("echo or([1, 2, 3], [2, 3, 4])")
assertNoExOutput()
assertPluginError(true) assertPluginError(true)
assertPluginErrorMessage("E745: Using a List as a Number") assertPluginErrorMessage("E745: Using a List as a Number")
} }
@@ -51,7 +50,6 @@ class OrFunctionTest : VimTestCase() {
@Test @Test
fun `test or function with dict causes error`() { fun `test or function with dict causes error`() {
enterCommand("echo or({1: 2, 3: 4}, {3: 4, 5: 6})") enterCommand("echo or({1: 2, 3: 4}, {3: 4, 5: 6})")
assertNoExOutput()
assertPluginError(true) assertPluginError(true)
assertPluginErrorMessage("E728: Using a Dictionary as a Number") assertPluginErrorMessage("E728: Using a Dictionary as a Number")
} }
@@ -59,7 +57,6 @@ class OrFunctionTest : VimTestCase() {
@Test @Test
fun `test or function with float causes error`() { fun `test or function with float causes error`() {
enterCommand("echo or(1.5, 2.5)") enterCommand("echo or(1.5, 2.5)")
assertNoExOutput()
assertPluginError(true) assertPluginError(true)
assertPluginErrorMessage("E805: Using a Float as a Number") assertPluginErrorMessage("E805: Using a Float as a Number")
} }

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright 2003-2025 The IdeaVim authors * Copyright 2003-2026 The IdeaVim authors
* *
* Use of this source code is governed by an MIT-style * Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE.txt file or at * license that can be found in the LICENSE.txt file or at
@@ -43,7 +43,6 @@ class XorFunctionTest : VimTestCase() {
@Test @Test
fun `test xor function with list causes error`() { fun `test xor function with list causes error`() {
enterCommand("echo xor([1, 2, 3], [2, 3, 4])") enterCommand("echo xor([1, 2, 3], [2, 3, 4])")
assertNoExOutput()
assertPluginError(true) assertPluginError(true)
assertPluginErrorMessage("E745: Using a List as a Number") assertPluginErrorMessage("E745: Using a List as a Number")
} }
@@ -51,7 +50,6 @@ class XorFunctionTest : VimTestCase() {
@Test @Test
fun `test xor function with dict causes error`() { fun `test xor function with dict causes error`() {
enterCommand("echo xor({1: 2, 3: 4}, {3: 4, 5: 6})") enterCommand("echo xor({1: 2, 3: 4}, {3: 4, 5: 6})")
assertNoExOutput()
assertPluginError(true) assertPluginError(true)
assertPluginErrorMessage("E728: Using a Dictionary as a Number") assertPluginErrorMessage("E728: Using a Dictionary as a Number")
} }
@@ -59,7 +57,6 @@ class XorFunctionTest : VimTestCase() {
@Test @Test
fun `test xor function with float causes error`() { fun `test xor function with float causes error`() {
enterCommand("echo xor(1.5, 2.5)") enterCommand("echo xor(1.5, 2.5)")
assertNoExOutput()
assertPluginError(true) assertPluginError(true)
assertPluginErrorMessage("E805: Using a Float as a Number") assertPluginErrorMessage("E805: Using a Float as a Number")
} }

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright 2003-2025 The IdeaVim authors * Copyright 2003-2026 The IdeaVim authors
* *
* Use of this source code is governed by an MIT-style * Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE.txt file or at * license that can be found in the LICENSE.txt file or at
@@ -33,7 +33,6 @@ class ToLowerFunctionTest : VimTestCase() {
@Test @Test
fun `test tolower with list causes error`() { fun `test tolower with list causes error`() {
enterCommand("echo tolower([1, 2, 3])") enterCommand("echo tolower([1, 2, 3])")
assertNoExOutput()
assertPluginError(true) assertPluginError(true)
assertPluginErrorMessage("E730: Using a List as a String") assertPluginErrorMessage("E730: Using a List as a String")
} }
@@ -41,7 +40,6 @@ class ToLowerFunctionTest : VimTestCase() {
@Test @Test
fun `test tolower with dict causes error`() { fun `test tolower with dict causes error`() {
enterCommand("echo tolower({1: 2, 3: 4})") enterCommand("echo tolower({1: 2, 3: 4})")
assertNoExOutput()
assertPluginError(true) assertPluginError(true)
assertPluginErrorMessage("E731: Using a Dictionary as a String") assertPluginErrorMessage("E731: Using a Dictionary as a String")
} }

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright 2003-2025 The IdeaVim authors * Copyright 2003-2026 The IdeaVim authors
* *
* Use of this source code is governed by an MIT-style * Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE.txt file or at * license that can be found in the LICENSE.txt file or at
@@ -33,7 +33,6 @@ class ToUpperFunctionTest : VimTestCase() {
@Test @Test
fun `test toupper with list causes error`() { fun `test toupper with list causes error`() {
enterCommand("echo toupper([1, 2, 3])") enterCommand("echo toupper([1, 2, 3])")
assertNoExOutput()
assertPluginError(true) assertPluginError(true)
assertPluginErrorMessage("E730: Using a List as a String") assertPluginErrorMessage("E730: Using a List as a String")
} }
@@ -41,7 +40,6 @@ class ToUpperFunctionTest : VimTestCase() {
@Test @Test
fun `test toupper with dict causes error`() { fun `test toupper with dict causes error`() {
enterCommand("echo toupper({1: 2, 3: 4})") enterCommand("echo toupper({1: 2, 3: 4})")
assertNoExOutput()
assertPluginError(true) assertPluginError(true)
assertPluginErrorMessage("E731: Using a Dictionary as a String") assertPluginErrorMessage("E731: Using a Dictionary as a String")
} }

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright 2003-2023 The IdeaVim authors * Copyright 2003-2026 The IdeaVim authors
* *
* Use of this source code is governed by an MIT-style * Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE.txt file or at * license that can be found in the LICENSE.txt file or at
@@ -207,7 +207,12 @@ class FunctionDeclarationTest : VimTestCase() {
typeText(commandToKeys("echo F1()")) typeText(commandToKeys("echo F1()"))
assertPluginError(true) assertPluginError(true)
assertPluginErrorMessage("E121: Undefined variable: x") assertPluginErrorMessage("E121: Undefined variable: x")
assertExOutput("0") assertExOutput(
"""
E121: Undefined variable: x
0
""".trimIndent()
)
typeText(commandToKeys("delf! F1")) typeText(commandToKeys("delf! F1"))
typeText(commandToKeys("delf! F2")) typeText(commandToKeys("delf! F2"))

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright 2003-2023 The IdeaVim authors * Copyright 2003-2026 The IdeaVim authors
* *
* Use of this source code is governed by an MIT-style * Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE.txt file or at * license that can be found in the LICENSE.txt file or at
@@ -154,7 +154,12 @@ class TryCatchTest : VimTestCase() {
), ),
) )
assertPluginError(true) assertPluginError(true)
assertExOutput("finally block") assertExOutput(
"""
finally block
my exception
""".trimIndent()
)
} }
@Test @Test

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright 2003-2023 The IdeaVim authors * Copyright 2003-2026 The IdeaVim authors
* *
* Use of this source code is governed by an MIT-style * Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE.txt file or at * license that can be found in the LICENSE.txt file or at
@@ -223,7 +223,7 @@ class SearchGroupTest : VimTestCase() {
) { ) {
enterCommand("set nowrapscan") enterCommand("set nowrapscan")
} }
assertPluginError(false) assertPluginError(true)
assertPluginErrorMessage("E385: Search hit BOTTOM without match for: one") assertPluginErrorMessage("E385: Search hit BOTTOM without match for: one")
} }
@@ -242,7 +242,7 @@ class SearchGroupTest : VimTestCase() {
three three
""".trimIndent() """.trimIndent()
) )
assertPluginError(false) assertPluginError(true)
assertPluginErrorMessage("E486: Pattern not found: banana") assertPluginErrorMessage("E486: Pattern not found: banana")
} }
@@ -282,7 +282,7 @@ class SearchGroupTest : VimTestCase() {
) { ) {
enterCommand("set nowrapscan") enterCommand("set nowrapscan")
} }
assertPluginError(false) assertPluginError(true)
assertPluginErrorMessage("E384: Search hit TOP without match for: three") assertPluginErrorMessage("E384: Search hit TOP without match for: three")
} }
@@ -301,7 +301,7 @@ class SearchGroupTest : VimTestCase() {
three three
""".trimIndent() """.trimIndent()
) )
assertPluginError(false) assertPluginError(true)
assertPluginErrorMessage("E486: Pattern not found: banana") assertPluginErrorMessage("E486: Pattern not found: banana")
} }
@@ -615,7 +615,7 @@ class SearchGroupTest : VimTestCase() {
) )
enterCommand("set nowrapscan") enterCommand("set nowrapscan")
typeText("10", "/", searchCommand("one")) typeText("10", "/", searchCommand("one"))
assertPluginError(false) assertPluginError(true)
assertPluginErrorMessage("E385: Search hit BOTTOM without match for: one") assertPluginErrorMessage("E385: Search hit BOTTOM without match for: one")
assertPosition(2, 0) assertPosition(2, 0)
} }
@@ -679,7 +679,7 @@ class SearchGroupTest : VimTestCase() {
) )
enterCommand("set nowrapscan") enterCommand("set nowrapscan")
typeText("12", "?one<CR>") typeText("12", "?one<CR>")
assertPluginError(false) assertPluginError(true)
assertPluginErrorMessage("E384: Search hit TOP without match for: one") assertPluginErrorMessage("E384: Search hit TOP without match for: one")
assertPosition(8, 0) assertPosition(8, 0)
} }

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright 2003-2023 The IdeaVim authors * Copyright 2003-2026 The IdeaVim authors
* *
* Use of this source code is governed by an MIT-style * Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE.txt file or at * license that can be found in the LICENSE.txt file or at
@@ -12,7 +12,25 @@ import com.maddyhome.idea.vim.helper.EngineMessageHelper
import org.jetbrains.annotations.PropertyKey import org.jetbrains.annotations.PropertyKey
interface VimMessages { interface VimMessages {
/**
* Displays an informational message to the user.
* The message panel closes on any keystroke and passes the key through to the editor.
*/
fun showMessage(editor: VimEditor, message: String?)
/**
* Displays an error message to the user (typically in red).
* The message panel closes on any keystroke and passes the key through to the editor.
*/
fun showErrorMessage(editor: VimEditor, message: String?)
/**
* Legacy method for displaying messages.
* @deprecated Use [showMessage] or [showErrorMessage] instead.
*/
@Deprecated("Use showMessage or showErrorMessage instead", ReplaceWith("showMessage(editor, message)"))
fun showStatusBarMessage(editor: VimEditor?, message: String?) fun showStatusBarMessage(editor: VimEditor?, message: String?)
fun getStatusBarMessage(): String? fun getStatusBarMessage(): String?
fun clearStatusBarMessage() fun clearStatusBarMessage()
fun indicateError() fun indicateError()
@@ -28,13 +46,4 @@ interface VimMessages {
fun message(@PropertyKey(resourceBundle = EngineMessageHelper.BUNDLE) key: String, vararg params: Any): String fun message(@PropertyKey(resourceBundle = EngineMessageHelper.BUNDLE) key: String, vararg params: Any): String
fun updateStatusBar(editor: VimEditor) fun updateStatusBar(editor: VimEditor)
fun showMessage(editor: VimEditor, message: String) {
showStatusBarMessage(editor, message)
}
fun showErrorMessage(editor: VimEditor, message: String?) {
showStatusBarMessage(editor, message)
indicateError()
}
} }

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright 2003-2024 The IdeaVim authors * Copyright 2003-2026 The IdeaVim authors
* *
* Use of this source code is governed by an MIT-style * Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE.txt file or at * license that can be found in the LICENSE.txt file or at
@@ -29,7 +29,8 @@ interface VimOutputPanel {
* Note: The full text content is not updated in the display until [show] is invoked. * Note: The full text content is not updated in the display until [show] is invoked.
* *
* @param text The text to append. * @param text The text to append.
* @param isNewLine Whether to start the appended text on a new line. Defaults to true. * @param isNewLine Whether to start the appended text on a new line.
* @param messageType The type of message, used to determine text styling.
*/ */
fun addText(text: String, isNewLine: Boolean = true, messageType: MessageType = MessageType.STANDARD) fun addText(text: String, isNewLine: Boolean = true, messageType: MessageType = MessageType.STANDARD)
@@ -51,4 +52,4 @@ interface VimOutputPanel {
fun setContent(text: String) fun setContent(text: String)
fun clearText() fun clearText()
} }

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright 2003-2024 The IdeaVim authors * Copyright 2003-2026 The IdeaVim authors
* *
* Use of this source code is governed by an MIT-style * Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE.txt file or at * license that can be found in the LICENSE.txt file or at
@@ -26,8 +26,12 @@ interface VimOutputPanelService {
fun getCurrentOutputPanel(): VimOutputPanel? fun getCurrentOutputPanel(): VimOutputPanel?
/** /**
* Appends text to the existing output panel or creates a new one with the given text. * Appends text to the existing output panel or creates a new one with the given text and message type.
* Basic method that should be sufficient in most cases.
*/ */
fun output(editor: VimEditor, context: ExecutionContext, text: String) fun output(
} editor: VimEditor,
context: ExecutionContext,
text: String,
messageType: MessageType = MessageType.STANDARD,
)
}

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright 2003-2024 The IdeaVim authors * Copyright 2003-2026 The IdeaVim authors
* *
* Use of this source code is governed by an MIT-style * Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE.txt file or at * license that can be found in the LICENSE.txt file or at
@@ -13,9 +13,9 @@ abstract class VimOutputPanelServiceBase : VimOutputPanelService {
return getCurrentOutputPanel() ?: create(editor, context) return getCurrentOutputPanel() ?: create(editor, context)
} }
override fun output(editor: VimEditor, context: ExecutionContext, text: String) { override fun output(editor: VimEditor, context: ExecutionContext, text: String, messageType: MessageType) {
val panel = getOrCreate(editor, context) val panel = getOrCreate(editor, context)
panel.addText(text) panel.addText(text, true, messageType)
panel.show() panel.show()
} }
} }