Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add search widget #1054

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -101,10 +101,12 @@ import com.maddyhome.idea.vim.state.mode.inSelectMode
import com.maddyhome.idea.vim.state.mode.selectionType
import com.maddyhome.idea.vim.ui.ShowCmdOptionChangeListener
import com.maddyhome.idea.vim.ui.ShowCmdWidgetUpdater
import com.maddyhome.idea.vim.ui.widgets.search.searchWidgetOptionListener
import com.maddyhome.idea.vim.ui.widgets.macro.MacroWidgetListener
import com.maddyhome.idea.vim.ui.widgets.macro.macroWidgetOptionListener
import com.maddyhome.idea.vim.ui.widgets.mode.listeners.ModeWidgetListener
import com.maddyhome.idea.vim.ui.widgets.mode.modeWidgetOptionListener
import com.maddyhome.idea.vim.ui.widgets.search.SearchWidgetListener
import org.jetbrains.annotations.TestOnly
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
Expand Down Expand Up @@ -169,6 +171,10 @@ internal object VimListenerManager {
injector.listenersNotifier.myEditorListeners.add(modeWidgetListener)
injector.listenersNotifier.vimPluginListeners.add(modeWidgetListener)

val searchWidgetListener = SearchWidgetListener()
injector.listenersNotifier.searchListeners.add(searchWidgetListener)
injector.listenersNotifier.vimPluginListeners.add(searchWidgetListener)

val macroWidgetListener = MacroWidgetListener()
injector.listenersNotifier.macroRecordingListeners.add(macroWidgetListener)
injector.listenersNotifier.vimPluginListeners.add(macroWidgetListener)
Expand Down Expand Up @@ -205,8 +211,10 @@ internal object VimListenerManager {
// This code is executed after ideavimrc execution, so we trigger onGlobalOptionChanged just in case
optionGroup.addGlobalOptionChangeListener(Options.showmode, modeWidgetOptionListener)
optionGroup.addGlobalOptionChangeListener(Options.showmode, macroWidgetOptionListener)
optionGroup.addGlobalOptionChangeListener(Options.showmode, searchWidgetOptionListener)
modeWidgetOptionListener.onGlobalOptionChanged()
macroWidgetOptionListener.onGlobalOptionChanged()
searchWidgetOptionListener.onGlobalOptionChanged()

// Listen for and initialise new editors
EventFacade.getInstance().addEditorFactoryListener(VimEditorFactoryListener, VimPlugin.getInstance().onOffDisposable)
Expand All @@ -224,6 +232,7 @@ internal object VimListenerManager {
optionGroup.removeGlobalOptionChangeListener(Options.showcmd, ShowCmdOptionChangeListener)
optionGroup.removeGlobalOptionChangeListener(Options.showmode, modeWidgetOptionListener)
optionGroup.removeGlobalOptionChangeListener(Options.showmode, macroWidgetOptionListener)
optionGroup.removeGlobalOptionChangeListener(Options.showmode, searchWidgetOptionListener)
optionGroup.removeEffectiveOptionValueChangeListener(Options.guicursor, GuicursorChangeListener)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ open class IjVimSearchGroup : VimSearchGroupBase(), PersistentStateComponent<Ele

override fun clearSearchHighlight() {
showSearchHighlight = false
injector.listenersNotifier.notifySearchStopped()
updateSearchHighlights(false)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
/*
* Copyright 2003-2024 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
* https://opensource.org/licenses/MIT.
*/

package com.maddyhome.idea.vim.ui.widgets.search

import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project
import com.intellij.openapi.project.ProjectManager
import com.intellij.openapi.wm.StatusBarWidget
import com.intellij.openapi.wm.StatusBarWidgetFactory
import com.intellij.openapi.wm.WindowManager
import com.intellij.openapi.wm.impl.status.widget.StatusBarWidgetsManager
import com.maddyhome.idea.vim.VimPlugin
import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.api.globalOptions
import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.common.SearchListener
import com.maddyhome.idea.vim.helper.vimLastHighlighters
import com.maddyhome.idea.vim.newapi.ij
import com.maddyhome.idea.vim.ui.widgets.VimWidgetListener
import com.maddyhome.idea.vim.ui.widgets.mode.VimStatusBarWidget
import java.awt.Component

private const val ID = "IdeaVimSearch"

internal class SearchWidgetFactory : StatusBarWidgetFactory {

override fun getId(): String {
return ID
}

override fun getDisplayName(): String {
return "IdeaVim Search"
}

override fun createWidget(project: Project): StatusBarWidget {
return VimSearchWidget()
}

override fun isAvailable(project: Project): Boolean {
return VimPlugin.isEnabled() && injector.globalOptions().showmode
}
}

fun updateSearchWidget() {
val factory = StatusBarWidgetFactory.EP_NAME.findExtension(SearchWidgetFactory::class.java) ?: return
for (project in ProjectManager.getInstance().openProjects) {
val statusBarWidgetsManager = project.service<StatusBarWidgetsManager>()
statusBarWidgetsManager.updateWidget(factory)
}
}

class VimSearchWidget : StatusBarWidget, VimStatusBarWidget {
var content: String = ""
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Directly accessing the content here leaks incapsulation. Let's have special functions like setSearch(current, total) and clearSearch().
(function names are subject to change).


override fun ID(): String {
return ID
}

override fun getPresentation(): StatusBarWidget.WidgetPresentation {
return VimModeWidgetPresentation()
}

private inner class VimModeWidgetPresentation : StatusBarWidget.TextPresentation {
override fun getAlignment(): Float = Component.CENTER_ALIGNMENT

override fun getText(): String {
return content
}

override fun getTooltipText(): String {
return content.ifEmpty {
"No search in progress"
}
}
}
}

class SearchWidgetListener : SearchListener, VimWidgetListener({ updateSearchWidget() }) {

override fun searchUpdated(editor: VimEditor, offset: Int) {
val ijEditor = editor.ij
var currentHighlighter = 1
val numHighlighters = ijEditor.vimLastHighlighters?.size ?: 0
for (highlighter in ijEditor.vimLastHighlighters!!) {
if (highlighter.startOffset == offset) {
break
}
currentHighlighter++
}
for (project in ProjectManager.getInstance().openProjects) {
val searchWidget = getWidget(project) ?: continue
searchWidget.content = String.format("Occurrence: %s of %s", currentHighlighter, numHighlighters)
searchWidget.updateWidgetInStatusBar(ID, project)
}
}

override fun searchStopped() {
for (project in ProjectManager.getInstance().openProjects) {
val searchWidget = getWidget(project) ?: continue
searchWidget.content = ""
searchWidget.updateWidgetInStatusBar(ID, project)
}
}

private fun getWidget(project: Project): VimSearchWidget? {
val statusBar = WindowManager.getInstance()?.getStatusBar(project) ?: return null
return statusBar.getWidget(ID) as? VimSearchWidget
}

}

val searchWidgetOptionListener: VimWidgetListener = VimWidgetListener { updateSearchWidget() }
1 change: 1 addition & 0 deletions src/main/resources/META-INF/plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
<statusBarWidgetFactory id="IdeaVim-Icon" implementation="com.maddyhome.idea.vim.ui.StatusBarIconFactory" order="last, before IdeaVimMode"/>
<statusBarWidgetFactory id="IdeaVimShowCmd" implementation="com.maddyhome.idea.vim.ui.ShowCmdStatusBarWidgetFactory" order="first"/>
<statusBarWidgetFactory id="IdeaVimMacro" implementation="com.maddyhome.idea.vim.ui.widgets.macro.MacroWidgetFactory" order="first, after IdeaVimShowCmd"/>
<statusBarWidgetFactory id="IdeaVimSearch" implementation="com.maddyhome.idea.vim.ui.widgets.search.SearchWidgetFactory" order="first, after IdeaVimShowCmd"/>

<applicationService serviceImplementation="com.maddyhome.idea.vim.VimPlugin"/>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1340,13 +1340,15 @@ abstract class VimSearchGroupBase : VimSearchGroup {
} else if (lastPatternTrailing!![ppos + 1] == '?') {
Direction.BACKWARDS
} else {
injector.listenersNotifier.notifySearchUpdated(editor, res)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not a good place for running listeners. The finItOffset should be a clean function that returns the result of the search. If it's not clean, let's not make it worse at least.

return if (res == -1) null else Pair(res, motionType)
}
if (lastPatternTrailing!!.length - ppos > 2) {
ppos++
}
res = processSearchCommand(editor, lastPatternTrailing!!.substring(ppos + 1), res, 1, nextDir)?.first ?: -1
}
injector.listenersNotifier.notifySearchUpdated(editor, res)
return if (res == -1) null else Pair(res, motionType)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
* Copyright 2003-2024 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
* https://opensource.org/licenses/MIT.
*/

package com.maddyhome.idea.vim.common

import com.maddyhome.idea.vim.api.VimEditor

interface SearchListener {

fun searchUpdated(editor: VimEditor, offset: Int)
fun searchStopped()

}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ class VimListenersNotifier {
val modeChangeListeners: MutableCollection<ModeChangeListener> = ConcurrentLinkedDeque()
val myEditorListeners: MutableCollection<EditorListener> = ConcurrentLinkedDeque()
val macroRecordingListeners: MutableCollection<MacroRecordingListener> = ConcurrentLinkedDeque()
val searchListeners: MutableCollection<SearchListener> = ConcurrentLinkedDeque()
val vimPluginListeners: MutableCollection<VimPluginListener> = ConcurrentLinkedDeque()
val isReplaceCharListeners: MutableCollection<IsReplaceCharListener> = ConcurrentLinkedDeque()
val yankListeners: MutableCollection<VimYankListener> = ConcurrentLinkedDeque()
Expand Down Expand Up @@ -49,6 +50,16 @@ class VimListenersNotifier {
myEditorListeners.forEach { it.focusLost(editor) }
}

fun notifySearchUpdated(editor: VimEditor, offset: Int) {
if (!injector.enabler.isEnabled()) return // we remove all the listeners when turning the plugin off, but let's do it just in case
searchListeners.forEach { it.searchUpdated(editor, offset) }
}

fun notifySearchStopped() {
if (!injector.enabler.isEnabled()) return // we remove all the listeners when turning the plugin off, but let's do it just in case
searchListeners.forEach { it.searchStopped() }
}

fun notifyMacroRecordingStarted() {
if (!injector.enabler.isEnabled()) return // we remove all the listeners when turning the plugin off, but let's do it just in case
macroRecordingListeners.forEach { it.recordingStarted() }
Expand Down
Loading