mirror of
https://github.com/chylex/IntelliJ-IdeaVim.git
synced 2026-04-11 18:57:41 +02:00
Compare commits
24 Commits
customized
...
customized
| Author | SHA1 | Date | |
|---|---|---|---|
|
2d79870c0f
|
|||
|
551c6286ab
|
|||
|
e17e93d143
|
|||
|
18722c240c
|
|||
|
945fdf3fe9
|
|||
|
40e9d6ff7a
|
|||
|
6bce1110e5
|
|||
|
d8876b525a
|
|||
|
069815326a
|
|||
|
755bf21d35
|
|||
|
565ce9f9e4
|
|||
|
8cb78e26f8
|
|||
|
430bdc2a82
|
|||
|
fe55e3e6eb
|
|||
|
b7ac7acaf5
|
|||
|
8cd0e2c266
|
|||
|
902c005826
|
|||
|
b09ded236f
|
|||
|
221b5474c9
|
|||
|
2a91e67f39
|
|||
|
e2bd6a2828
|
|||
|
52ff8012cc
|
|||
|
0bac02e40e
|
|||
|
33740616da
|
@@ -20,7 +20,7 @@ ideaVersion=2026.1
|
||||
# Values for type: https://plugins.jetbrains.com/docs/intellij/tools-gradle-intellij-plugin.html#intellij-extension-type
|
||||
ideaType=IU
|
||||
instrumentPluginCode=true
|
||||
version=chylex-56
|
||||
version=chylex-55
|
||||
javaVersion=21
|
||||
remoteRobotVersion=0.11.23
|
||||
antlrVersion=4.10.1
|
||||
|
||||
@@ -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
|
||||
* license that can be found in the LICENSE.txt file or at
|
||||
@@ -9,9 +9,7 @@
|
||||
package com.maddyhome.idea.vim.newapi
|
||||
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.project.ProjectManager
|
||||
import com.intellij.openapi.wm.WindowManager
|
||||
import com.maddyhome.idea.vim.api.MessageType
|
||||
import com.maddyhome.idea.vim.api.VimEditor
|
||||
import com.maddyhome.idea.vim.api.VimMessagesBase
|
||||
import com.maddyhome.idea.vim.api.globalOptions
|
||||
@@ -25,56 +23,43 @@ internal class IjVimMessages : VimMessagesBase() {
|
||||
private var message: String? = null
|
||||
private var error = false
|
||||
private var lastBeepTimeMillis = 0L
|
||||
private var allowClearStatusBarMessage = true
|
||||
|
||||
override fun showStatusBarMessage(editor: VimEditor?, message: String?) {
|
||||
fun setStatusBarMessage(project: Project, message: String?) {
|
||||
WindowManager.getInstance().getStatusBar(project)?.let {
|
||||
it.info = if (message.isNullOrBlank()) "" else "Vim - $message"
|
||||
}
|
||||
}
|
||||
override fun showMessage(editor: VimEditor, message: String?) {
|
||||
showMessageInternal(editor, message, MessageType.STANDARD)
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
val project = editor?.ij?.project
|
||||
if (project != null) {
|
||||
setStatusBarMessage(project, message)
|
||||
} else {
|
||||
// TODO: We really shouldn't set the status bar text for other projects. That's rude.
|
||||
ProjectManager.getInstance().openProjects.forEach {
|
||||
setStatusBarMessage(it, message)
|
||||
}
|
||||
if (message.isNullOrBlank()) {
|
||||
clearStatusBarMessage()
|
||||
return
|
||||
}
|
||||
|
||||
// Redraw happens automatically based on changes or scrolling. If we've just set the message (e.g., searching for a
|
||||
// string, hitting the bottom and scrolling to the top), make sure we don't immediately clear it when scrolling.
|
||||
allowClearStatusBarMessage = false
|
||||
ApplicationManager.getApplication().invokeLater {
|
||||
allowClearStatusBarMessage = true
|
||||
val context = injector.executionContextManager.getEditorExecutionContext(editor)
|
||||
injector.outputPanel.output(editor, context, message, messageType)
|
||||
}
|
||||
|
||||
@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
|
||||
|
||||
// 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() {
|
||||
val currentMessage = message
|
||||
if (currentMessage.isNullOrEmpty()) return
|
||||
|
||||
// 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 = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
if (message.isNullOrEmpty()) return
|
||||
injector.outputPanel.getCurrentOutputPanel()?.close()
|
||||
message = null
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import com.intellij.openapi.editor.Editor
|
||||
import com.intellij.openapi.wm.impl.IdeBackgroundUtil
|
||||
import com.intellij.openapi.wm.impl.ToolWindowManagerImpl
|
||||
import com.intellij.ui.ClientProperty
|
||||
import com.intellij.ui.JBColor
|
||||
import com.intellij.ui.components.JBPanel
|
||||
import com.intellij.ui.components.JBScrollPane
|
||||
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.globalOptions
|
||||
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.selectEditorFont
|
||||
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.KeyAdapter
|
||||
import java.awt.event.KeyEvent
|
||||
import java.lang.ref.WeakReference
|
||||
import javax.swing.JComponent
|
||||
import javax.swing.JLabel
|
||||
import javax.swing.JRootPane
|
||||
import javax.swing.JScrollPane
|
||||
import javax.swing.JTextArea
|
||||
import javax.swing.JTextPane
|
||||
import javax.swing.KeyStroke
|
||||
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.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 {
|
||||
private val myEditorRef: WeakReference<Editor> = editorRef
|
||||
val editor: Editor? get() = myEditorRef.get()
|
||||
class OutputPanel private constructor(
|
||||
private val editor: Editor,
|
||||
) : JBPanel<OutputPanel>(), VimOutputPanel {
|
||||
|
||||
val myLabel: JLabel = JLabel("more")
|
||||
private val myText = JTextArea()
|
||||
private val myScrollPane: JScrollPane =
|
||||
JBScrollPane(myText, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER)
|
||||
private val myAdapter: ComponentAdapter
|
||||
private var myLineHeight = 0
|
||||
private val textPane = JTextPane()
|
||||
private val resizeAdapter: ComponentAdapter
|
||||
private var defaultForeground: Color? = null
|
||||
|
||||
private var myOldGlass: JComponent? = null
|
||||
private var myOldLayout: LayoutManager? = null
|
||||
private var myWasOpaque = false
|
||||
private var glassPane: JComponent? = null
|
||||
private var originalLayout: LayoutManager? = null
|
||||
private var wasOpaque = false
|
||||
|
||||
var myActive: Boolean = false
|
||||
var active: Boolean = false
|
||||
private val segments = mutableListOf<TextLine>()
|
||||
|
||||
val isActive: Boolean
|
||||
get() = myActive
|
||||
private val labelComponent: JLabel = JLabel("more")
|
||||
private val scrollPane: JScrollPane =
|
||||
JBScrollPane(textPane, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER)
|
||||
private var cachedLineHeight = 0
|
||||
private var isSingleLine = false
|
||||
|
||||
init {
|
||||
// Create a text editor for the text and a label for the prompt
|
||||
val layout = BorderLayout(0, 0)
|
||||
setLayout(layout)
|
||||
add(myScrollPane, BorderLayout.CENTER)
|
||||
add(myLabel, BorderLayout.SOUTH)
|
||||
textPane.isEditable = false
|
||||
textPane.caret = object : DefaultCaret() {
|
||||
override fun setVisible(v: Boolean) {
|
||||
super.setVisible(false)
|
||||
}
|
||||
}
|
||||
textPane.highlighter = null
|
||||
|
||||
// Set the text area read only, and support wrap
|
||||
myText.isEditable = false
|
||||
myText.setLineWrap(true)
|
||||
|
||||
myAdapter = object : ComponentAdapter() {
|
||||
resizeAdapter = object : ComponentAdapter() {
|
||||
override fun componentResized(e: ComponentEvent?) {
|
||||
positionPanel()
|
||||
}
|
||||
}
|
||||
|
||||
// Setup some listeners to handle keystrokes
|
||||
val moreKeyListener = MoreKeyListener()
|
||||
addKeyListener(moreKeyListener)
|
||||
myText.addKeyListener(moreKeyListener)
|
||||
// Suppress the fancy frame background used in the Islands theme
|
||||
ClientProperty.putRecursive(this, IdeBackgroundUtil.NO_BACKGROUND, true)
|
||||
putClientProperty(ToolWindowManagerImpl.PARENT_COMPONENT, editor.component)
|
||||
|
||||
// Suppress the fancy frame background used in the Islands theme, which comes from a custom Graphics implementation
|
||||
// applied to the IdeRoot, and used to paint all children, including this panel. This client property is checked by
|
||||
// JBPanel.getComponentGraphics to give us the original Graphics, opting out of the fancy painting.
|
||||
ClientProperty.putRecursive<Boolean?>(this, IdeBackgroundUtil.NO_BACKGROUND, true)
|
||||
editor?.let { putClientProperty(ToolWindowManagerImpl.PARENT_COMPONENT, it.component) }
|
||||
// Initialize panel
|
||||
setLayout(BorderLayout(0, 0))
|
||||
add(scrollPane, BorderLayout.CENTER)
|
||||
add(labelComponent, BorderLayout.SOUTH)
|
||||
|
||||
val keyListener = OutputPanelKeyListener()
|
||||
addKeyListener(keyListener)
|
||||
textPane.addKeyListener(keyListener)
|
||||
|
||||
updateUI()
|
||||
}
|
||||
|
||||
// Called automatically when the LAF is changed and the component is visible, and manually by the LAF listener handler
|
||||
override fun updateUI() {
|
||||
super.updateUI()
|
||||
|
||||
setBorder(ExPanelBorder())
|
||||
|
||||
// Swing uses a bad pattern of calling updateUI() from the constructor. At this moment, all these variables are null
|
||||
@Suppress("SENSELESS_COMPARISON")
|
||||
if (myText != null && myLabel != null && myScrollPane != null) {
|
||||
if (textPane != null && labelComponent != null && scrollPane != null) {
|
||||
setFontForElements()
|
||||
myText.setBorder(null)
|
||||
myScrollPane.setBorder(null)
|
||||
myLabel.setForeground(myText.getForeground())
|
||||
|
||||
// Make sure the panel is positioned correctly in case we're changing font size
|
||||
textPane.setBorder(null)
|
||||
scrollPane.setBorder(null)
|
||||
labelComponent.setForeground(textPane.getForeground())
|
||||
positionPanel()
|
||||
}
|
||||
}
|
||||
|
||||
override var text: String
|
||||
get() = myText.text
|
||||
get() = textPane.getText() ?: ""
|
||||
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")
|
||||
myText.text = newValue
|
||||
val ed = editor
|
||||
if (ed != null) {
|
||||
myText.setFont(selectEditorFont(ed, newValue))
|
||||
}
|
||||
myText.setCaretPosition(0)
|
||||
if (newValue.isNotEmpty()) {
|
||||
activate()
|
||||
}
|
||||
segments.clear()
|
||||
if (newValue.isEmpty()) return
|
||||
segments.add(TextLine(newValue, null))
|
||||
}
|
||||
|
||||
override var label: String
|
||||
get() = myLabel.text ?: ""
|
||||
get() = labelComponent.text
|
||||
set(value) {
|
||||
myLabel.text = value
|
||||
val ed = editor
|
||||
if (ed != null) {
|
||||
myLabel.setFont(selectEditorFont(ed, value))
|
||||
}
|
||||
labelComponent.text = value
|
||||
}
|
||||
|
||||
override fun addText(text: String, isNewLine: Boolean, messageType: MessageType) {
|
||||
if (this.text.isNotEmpty() && isNewLine) {
|
||||
this.text += "\n$text"
|
||||
/**
|
||||
* Sets styled text with multiple segments, each potentially having a different color.
|
||||
*/
|
||||
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 {
|
||||
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() {
|
||||
segments.clear()
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
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) {
|
||||
|
||||
if (isAtEnd) {
|
||||
close(key)
|
||||
return
|
||||
@@ -197,214 +237,262 @@ class OutputPanel(editorRef: WeakReference<Editor>) : JBPanel<OutputPanel?>(), V
|
||||
|
||||
override fun getForeground(): Color? {
|
||||
@Suppress("SENSELESS_COMPARISON")
|
||||
if (myText == null) {
|
||||
// Swing uses a bad pattern of calling getForeground() from the constructor. At this moment, `myText` is null.
|
||||
if (textPane == null) {
|
||||
return super.getForeground()
|
||||
}
|
||||
return myText.getForeground()
|
||||
return textPane.getForeground()
|
||||
}
|
||||
|
||||
override fun getBackground(): Color? {
|
||||
@Suppress("SENSELESS_COMPARISON")
|
||||
if (myText == null) {
|
||||
// Swing uses a bad pattern of calling getBackground() from the constructor. At this moment, `myText` is null.
|
||||
if (textPane == null) {
|
||||
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) {
|
||||
if (!myActive) return
|
||||
myActive = false
|
||||
myText.text = ""
|
||||
val ed = editor
|
||||
if (refocusOwningEditor && ed != null) {
|
||||
requestFocus(ed.contentComponent)
|
||||
if (!active) return
|
||||
active = false
|
||||
clearText()
|
||||
textPane.text = ""
|
||||
if (refocusOwningEditor) {
|
||||
requestFocus(editor.contentComponent)
|
||||
}
|
||||
if (myOldGlass != null) {
|
||||
myOldGlass!!.removeComponentListener(myAdapter)
|
||||
myOldGlass!!.isVisible = false
|
||||
myOldGlass!!.remove(this)
|
||||
myOldGlass!!.setOpaque(myWasOpaque)
|
||||
myOldGlass!!.setLayout(myOldLayout)
|
||||
if (glassPane != null) {
|
||||
glassPane!!.removeComponentListener(resizeAdapter)
|
||||
glassPane!!.isVisible = false
|
||||
glassPane!!.remove(this)
|
||||
glassPane!!.setOpaque(wasOpaque)
|
||||
glassPane!!.setLayout(originalLayout)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns on the more window for the given editor
|
||||
* Turns on the output panel for the given editor.
|
||||
*/
|
||||
fun activate() {
|
||||
val ed = editor ?: return
|
||||
val root = SwingUtilities.getRootPane(ed.contentComponent)
|
||||
deactivateOldGlass(root)
|
||||
disableOldGlass()
|
||||
|
||||
setFontForElements()
|
||||
positionPanel()
|
||||
|
||||
if (myOldGlass != null) {
|
||||
myOldGlass!!.isVisible = true
|
||||
if (glassPane != null) {
|
||||
glassPane!!.isVisible = true
|
||||
}
|
||||
|
||||
myActive = true
|
||||
requestFocus(myText)
|
||||
active = true
|
||||
requestFocus(textPane)
|
||||
}
|
||||
|
||||
private fun deactivateOldGlass(root: JRootPane?) {
|
||||
if (root == null) return
|
||||
myOldGlass = root.getGlassPane() as JComponent?
|
||||
if (myOldGlass != 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
|
||||
private fun disableOldGlass() {
|
||||
val root = SwingUtilities.getRootPane(editor.contentComponent) ?: return
|
||||
glassPane = root.getGlassPane() as JComponent?
|
||||
if (glassPane == null) {
|
||||
return
|
||||
}
|
||||
|
||||
size = scroll.size
|
||||
|
||||
myLineHeight = myText.getFontMetrics(myText.getFont()).height
|
||||
val count: Int = countLines(myText.getText())
|
||||
val visLines = size.height / myLineHeight - 1
|
||||
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)
|
||||
}
|
||||
}
|
||||
originalLayout = glassPane!!.layout
|
||||
wasOpaque = glassPane!!.isOpaque
|
||||
glassPane!!.setLayout(null)
|
||||
glassPane!!.setOpaque(false)
|
||||
glassPane!!.add(this)
|
||||
glassPane!!.addComponentListener(resizeAdapter)
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
close(null)
|
||||
}
|
||||
|
||||
private class MoreKeyListener : KeyAdapter() {
|
||||
/**
|
||||
* Invoked when a key has been pressed.
|
||||
*/
|
||||
fun close(key: KeyStroke?) {
|
||||
val passKeyBack = isSingleLine
|
||||
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) {
|
||||
val currentPanel: VimOutputPanel = injector.outputPanel.getCurrentOutputPanel() ?: return
|
||||
|
||||
val keyCode = e.getKeyCode()
|
||||
val keyChar = e.getKeyChar()
|
||||
val keyChar = e.keyChar
|
||||
val modifiers = e.modifiersEx
|
||||
val keyStroke = if (keyChar == KeyEvent.CHAR_UNDEFINED)
|
||||
KeyStroke.getKeyStroke(keyCode, modifiers)
|
||||
else
|
||||
KeyStroke.getKeyStroke(keyChar, modifiers)
|
||||
val keyStroke = KeyStroke.getKeyStroke(keyChar, modifiers)
|
||||
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 {
|
||||
override fun lookAndFeelChanged(source: LafManager) {
|
||||
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()) {
|
||||
val editor = (vimEditor as IjVimEditor).editor
|
||||
if (!isPanelActive(editor)) continue
|
||||
@@ -414,41 +502,24 @@ class OutputPanel(editorRef: WeakReference<Editor>) : JBPanel<OutputPanel?>(), V
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val LOG: VimLogger = injector.getLogger<OutputPanel>(OutputPanel::class.java)
|
||||
|
||||
fun getNullablePanel(editor: Editor): OutputPanel? {
|
||||
return editor.vimMorePanel as? OutputPanel
|
||||
return editor.vimMorePanel as OutputPanel?
|
||||
}
|
||||
|
||||
fun isPanelActive(editor: Editor): Boolean {
|
||||
return getNullablePanel(editor)?.myActive ?: false
|
||||
return getNullablePanel(editor) != null
|
||||
}
|
||||
|
||||
fun getInstance(editor: Editor): OutputPanel {
|
||||
var panel: OutputPanel? = getNullablePanel(editor)
|
||||
if (panel == null) {
|
||||
panel = OutputPanel(WeakReference(editor))
|
||||
panel = OutputPanel(editor)
|
||||
editor.vimMorePanel = 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?)
|
||||
|
||||
@@ -22,11 +22,11 @@ class IjOutputPanelService : VimOutputPanelServiceBase() {
|
||||
private var activeOutputPanel: WeakReference<VimOutputPanel>? = null
|
||||
|
||||
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 {
|
||||
val panel = OutputPanel(WeakReference(editor.ij))
|
||||
val panel = OutputPanel.getInstance(editor.ij)
|
||||
activeOutputPanel = WeakReference(panel)
|
||||
return panel
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
@@ -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
|
||||
* license that can be found in the LICENSE.txt file or at
|
||||
@@ -134,7 +134,7 @@ class CmdCommandTest : VimTestCase() {
|
||||
VimPlugin.getCommand().resetAliases()
|
||||
configureByText("\n")
|
||||
typeText(commandToKeys("command! -range Error echo <args>"))
|
||||
assertPluginError(false)
|
||||
assertPluginError(true)
|
||||
kotlin.test.assertEquals("'-range' is not supported by `command`", injector.messages.getStatusBarMessage())
|
||||
}
|
||||
|
||||
@@ -143,7 +143,7 @@ class CmdCommandTest : VimTestCase() {
|
||||
VimPlugin.getCommand().resetAliases()
|
||||
configureByText("\n")
|
||||
typeText(commandToKeys("command! -complete=color Error echo <args>"))
|
||||
assertPluginError(false)
|
||||
assertPluginError(true)
|
||||
kotlin.test.assertEquals("'-complete' is not supported by `command`", injector.messages.getStatusBarMessage())
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
* license that can be found in the LICENSE.txt file or at
|
||||
@@ -24,7 +24,6 @@ class ExecuteCommandTest : VimTestCase() {
|
||||
fun `test execute with range`() {
|
||||
configureByText("\n")
|
||||
typeText(commandToKeys("1,2execute 'echo 42'"))
|
||||
assertNoExOutput()
|
||||
assertPluginError(true)
|
||||
}
|
||||
|
||||
|
||||
@@ -72,7 +72,6 @@ class HistoryCommandTest : VimTestCase() {
|
||||
fun `test history with 'history' option set to 0 shows nothing`() {
|
||||
enterCommand("set history=0")
|
||||
enterCommand("history")
|
||||
assertNoExOutput()
|
||||
assertPluginError(false)
|
||||
assertPluginErrorMessage("'history' option is zero")
|
||||
}
|
||||
|
||||
@@ -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
|
||||
* license that can be found in the LICENSE.txt file or at
|
||||
@@ -43,7 +43,6 @@ class AndFunctionTest : VimTestCase() {
|
||||
@Test
|
||||
fun `test and function with list causes error`() {
|
||||
enterCommand("echo and([1, 2, 3], [2, 3, 4])")
|
||||
assertNoExOutput()
|
||||
assertPluginError(true)
|
||||
assertPluginErrorMessage("E745: Using a List as a Number")
|
||||
}
|
||||
@@ -51,7 +50,6 @@ class AndFunctionTest : VimTestCase() {
|
||||
@Test
|
||||
fun `test and function with dict causes error`() {
|
||||
enterCommand("echo and({1: 2, 3: 4}, {3: 4, 5: 6})")
|
||||
assertNoExOutput()
|
||||
assertPluginError(true)
|
||||
assertPluginErrorMessage("E728: Using a Dictionary as a Number")
|
||||
}
|
||||
@@ -59,7 +57,6 @@ class AndFunctionTest : VimTestCase() {
|
||||
@Test
|
||||
fun `test and function with float causes error`() {
|
||||
enterCommand("echo and(1.5, 2.5)")
|
||||
assertNoExOutput()
|
||||
assertPluginError(true)
|
||||
assertPluginErrorMessage("E805: Using a Float as a Number")
|
||||
}
|
||||
|
||||
@@ -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
|
||||
* license that can be found in the LICENSE.txt file or at
|
||||
@@ -28,7 +28,6 @@ class InvertFunctionTest : VimTestCase() {
|
||||
@Test
|
||||
fun `test invert function with list causes error`() {
|
||||
enterCommand("echo invert([1, 2, 3])")
|
||||
assertNoExOutput()
|
||||
assertPluginError(true)
|
||||
assertPluginErrorMessage("E745: Using a List as a Number")
|
||||
}
|
||||
@@ -36,7 +35,6 @@ class InvertFunctionTest : VimTestCase() {
|
||||
@Test
|
||||
fun `test invert function with dict causes error`() {
|
||||
enterCommand("echo invert({1: 2, 3: 4})")
|
||||
assertNoExOutput()
|
||||
assertPluginError(true)
|
||||
assertPluginErrorMessage("E728: Using a Dictionary as a Number")
|
||||
}
|
||||
@@ -44,7 +42,6 @@ class InvertFunctionTest : VimTestCase() {
|
||||
@Test
|
||||
fun `test invert function with float causes error`() {
|
||||
enterCommand("echo invert(1.5)")
|
||||
assertNoExOutput()
|
||||
assertPluginError(true)
|
||||
assertPluginErrorMessage("E805: Using a Float as a Number")
|
||||
}
|
||||
|
||||
@@ -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
|
||||
* license that can be found in the LICENSE.txt file or at
|
||||
@@ -43,7 +43,6 @@ class OrFunctionTest : VimTestCase() {
|
||||
@Test
|
||||
fun `test or function with list causes error`() {
|
||||
enterCommand("echo or([1, 2, 3], [2, 3, 4])")
|
||||
assertNoExOutput()
|
||||
assertPluginError(true)
|
||||
assertPluginErrorMessage("E745: Using a List as a Number")
|
||||
}
|
||||
@@ -51,7 +50,6 @@ class OrFunctionTest : VimTestCase() {
|
||||
@Test
|
||||
fun `test or function with dict causes error`() {
|
||||
enterCommand("echo or({1: 2, 3: 4}, {3: 4, 5: 6})")
|
||||
assertNoExOutput()
|
||||
assertPluginError(true)
|
||||
assertPluginErrorMessage("E728: Using a Dictionary as a Number")
|
||||
}
|
||||
@@ -59,7 +57,6 @@ class OrFunctionTest : VimTestCase() {
|
||||
@Test
|
||||
fun `test or function with float causes error`() {
|
||||
enterCommand("echo or(1.5, 2.5)")
|
||||
assertNoExOutput()
|
||||
assertPluginError(true)
|
||||
assertPluginErrorMessage("E805: Using a Float as a Number")
|
||||
}
|
||||
|
||||
@@ -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
|
||||
* license that can be found in the LICENSE.txt file or at
|
||||
@@ -43,7 +43,6 @@ class XorFunctionTest : VimTestCase() {
|
||||
@Test
|
||||
fun `test xor function with list causes error`() {
|
||||
enterCommand("echo xor([1, 2, 3], [2, 3, 4])")
|
||||
assertNoExOutput()
|
||||
assertPluginError(true)
|
||||
assertPluginErrorMessage("E745: Using a List as a Number")
|
||||
}
|
||||
@@ -51,7 +50,6 @@ class XorFunctionTest : VimTestCase() {
|
||||
@Test
|
||||
fun `test xor function with dict causes error`() {
|
||||
enterCommand("echo xor({1: 2, 3: 4}, {3: 4, 5: 6})")
|
||||
assertNoExOutput()
|
||||
assertPluginError(true)
|
||||
assertPluginErrorMessage("E728: Using a Dictionary as a Number")
|
||||
}
|
||||
@@ -59,7 +57,6 @@ class XorFunctionTest : VimTestCase() {
|
||||
@Test
|
||||
fun `test xor function with float causes error`() {
|
||||
enterCommand("echo xor(1.5, 2.5)")
|
||||
assertNoExOutput()
|
||||
assertPluginError(true)
|
||||
assertPluginErrorMessage("E805: Using a Float as a Number")
|
||||
}
|
||||
|
||||
@@ -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
|
||||
* license that can be found in the LICENSE.txt file or at
|
||||
@@ -33,7 +33,6 @@ class ToLowerFunctionTest : VimTestCase() {
|
||||
@Test
|
||||
fun `test tolower with list causes error`() {
|
||||
enterCommand("echo tolower([1, 2, 3])")
|
||||
assertNoExOutput()
|
||||
assertPluginError(true)
|
||||
assertPluginErrorMessage("E730: Using a List as a String")
|
||||
}
|
||||
@@ -41,7 +40,6 @@ class ToLowerFunctionTest : VimTestCase() {
|
||||
@Test
|
||||
fun `test tolower with dict causes error`() {
|
||||
enterCommand("echo tolower({1: 2, 3: 4})")
|
||||
assertNoExOutput()
|
||||
assertPluginError(true)
|
||||
assertPluginErrorMessage("E731: Using a Dictionary as a String")
|
||||
}
|
||||
|
||||
@@ -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
|
||||
* license that can be found in the LICENSE.txt file or at
|
||||
@@ -33,7 +33,6 @@ class ToUpperFunctionTest : VimTestCase() {
|
||||
@Test
|
||||
fun `test toupper with list causes error`() {
|
||||
enterCommand("echo toupper([1, 2, 3])")
|
||||
assertNoExOutput()
|
||||
assertPluginError(true)
|
||||
assertPluginErrorMessage("E730: Using a List as a String")
|
||||
}
|
||||
@@ -41,7 +40,6 @@ class ToUpperFunctionTest : VimTestCase() {
|
||||
@Test
|
||||
fun `test toupper with dict causes error`() {
|
||||
enterCommand("echo toupper({1: 2, 3: 4})")
|
||||
assertNoExOutput()
|
||||
assertPluginError(true)
|
||||
assertPluginErrorMessage("E731: Using a Dictionary as a String")
|
||||
}
|
||||
|
||||
@@ -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
|
||||
* license that can be found in the LICENSE.txt file or at
|
||||
@@ -207,7 +207,12 @@ class FunctionDeclarationTest : VimTestCase() {
|
||||
typeText(commandToKeys("echo F1()"))
|
||||
assertPluginError(true)
|
||||
assertPluginErrorMessage("E121: Undefined variable: x")
|
||||
assertExOutput("0")
|
||||
assertExOutput(
|
||||
"""
|
||||
E121: Undefined variable: x
|
||||
0
|
||||
""".trimIndent()
|
||||
)
|
||||
|
||||
typeText(commandToKeys("delf! F1"))
|
||||
typeText(commandToKeys("delf! F2"))
|
||||
|
||||
@@ -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
|
||||
* license that can be found in the LICENSE.txt file or at
|
||||
@@ -154,7 +154,12 @@ class TryCatchTest : VimTestCase() {
|
||||
),
|
||||
)
|
||||
assertPluginError(true)
|
||||
assertExOutput("finally block")
|
||||
assertExOutput(
|
||||
"""
|
||||
finally block
|
||||
my exception
|
||||
""".trimIndent()
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -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
|
||||
* license that can be found in the LICENSE.txt file or at
|
||||
@@ -223,7 +223,7 @@ class SearchGroupTest : VimTestCase() {
|
||||
) {
|
||||
enterCommand("set nowrapscan")
|
||||
}
|
||||
assertPluginError(false)
|
||||
assertPluginError(true)
|
||||
assertPluginErrorMessage("E385: Search hit BOTTOM without match for: one")
|
||||
}
|
||||
|
||||
@@ -242,7 +242,7 @@ class SearchGroupTest : VimTestCase() {
|
||||
three
|
||||
""".trimIndent()
|
||||
)
|
||||
assertPluginError(false)
|
||||
assertPluginError(true)
|
||||
assertPluginErrorMessage("E486: Pattern not found: banana")
|
||||
}
|
||||
|
||||
@@ -282,7 +282,7 @@ class SearchGroupTest : VimTestCase() {
|
||||
) {
|
||||
enterCommand("set nowrapscan")
|
||||
}
|
||||
assertPluginError(false)
|
||||
assertPluginError(true)
|
||||
assertPluginErrorMessage("E384: Search hit TOP without match for: three")
|
||||
}
|
||||
|
||||
@@ -301,7 +301,7 @@ class SearchGroupTest : VimTestCase() {
|
||||
three
|
||||
""".trimIndent()
|
||||
)
|
||||
assertPluginError(false)
|
||||
assertPluginError(true)
|
||||
assertPluginErrorMessage("E486: Pattern not found: banana")
|
||||
}
|
||||
|
||||
@@ -615,7 +615,7 @@ class SearchGroupTest : VimTestCase() {
|
||||
)
|
||||
enterCommand("set nowrapscan")
|
||||
typeText("10", "/", searchCommand("one"))
|
||||
assertPluginError(false)
|
||||
assertPluginError(true)
|
||||
assertPluginErrorMessage("E385: Search hit BOTTOM without match for: one")
|
||||
assertPosition(2, 0)
|
||||
}
|
||||
@@ -679,7 +679,7 @@ class SearchGroupTest : VimTestCase() {
|
||||
)
|
||||
enterCommand("set nowrapscan")
|
||||
typeText("12", "?one<CR>")
|
||||
assertPluginError(false)
|
||||
assertPluginError(true)
|
||||
assertPluginErrorMessage("E384: Search hit TOP without match for: one")
|
||||
assertPosition(8, 0)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
* 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
|
||||
|
||||
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 getStatusBarMessage(): String?
|
||||
fun clearStatusBarMessage()
|
||||
fun indicateError()
|
||||
@@ -28,13 +46,4 @@ interface VimMessages {
|
||||
fun message(@PropertyKey(resourceBundle = EngineMessageHelper.BUNDLE) key: String, vararg params: Any): String
|
||||
|
||||
fun updateStatusBar(editor: VimEditor)
|
||||
|
||||
fun showMessage(editor: VimEditor, message: String) {
|
||||
showStatusBarMessage(editor, message)
|
||||
}
|
||||
|
||||
fun showErrorMessage(editor: VimEditor, message: String?) {
|
||||
showStatusBarMessage(editor, message)
|
||||
indicateError()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
* 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.
|
||||
*
|
||||
* @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)
|
||||
|
||||
@@ -51,4 +52,4 @@ interface VimOutputPanel {
|
||||
|
||||
fun setContent(text: String)
|
||||
fun clearText()
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
* license that can be found in the LICENSE.txt file or at
|
||||
@@ -26,8 +26,12 @@ interface VimOutputPanelService {
|
||||
fun getCurrentOutputPanel(): VimOutputPanel?
|
||||
|
||||
/**
|
||||
* Appends text to the existing output panel or creates a new one with the given text.
|
||||
* Basic method that should be sufficient in most cases.
|
||||
* Appends text to the existing output panel or creates a new one with the given text and message type.
|
||||
*/
|
||||
fun output(editor: VimEditor, context: ExecutionContext, text: String)
|
||||
}
|
||||
fun output(
|
||||
editor: VimEditor,
|
||||
context: ExecutionContext,
|
||||
text: String,
|
||||
messageType: MessageType = MessageType.STANDARD,
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
* 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)
|
||||
}
|
||||
|
||||
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)
|
||||
panel.addText(text)
|
||||
panel.addText(text, true, messageType)
|
||||
panel.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user