Skip to content

Commit ac7a49e

Browse files
ctruedenclaude
andcommitted
Add initial support for vim key bindings
Many things work, but not everything, of course: * :g subcommands * q for recording macros into registers And probably many other things I don't use constantly and haven't noticed missing yet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 681df81 commit ac7a49e

6 files changed

Lines changed: 2459 additions & 11 deletions

File tree

‎pom.xml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
</parent>
1111

1212
<artifactId>script-editor</artifactId>
13-
<version>1.3.1-SNAPSHOT</version>
13+
<version>1.4.0-SNAPSHOT</version>
1414

1515
<name>SciJava Script Editor</name>
1616
<description>Script Editor and Interpreter for SciJava script languages.</description>

‎src/main/java/org/scijava/ui/swing/script/EditorPane.java‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import java.awt.Dimension;
3636
import java.awt.Font;
3737
import java.awt.event.ActionEvent;
38+
import java.awt.event.KeyEvent;
3839
import java.awt.event.MouseAdapter;
3940
import java.awt.event.MouseEvent;
4041
import java.io.BufferedReader;
@@ -92,6 +93,7 @@
9293
import org.scijava.script.ScriptHeaderService;
9394
import org.scijava.script.ScriptLanguage;
9495
import org.scijava.script.ScriptService;
96+
import org.scijava.ui.swing.script.vim.VimHandler;
9597
import org.scijava.util.FileUtils;
9698

9799
/**
@@ -121,6 +123,7 @@ public class EditorPane extends RSyntaxTextArea implements DocumentListener {
121123
private final ErrorParser errorHighlighter;
122124
private final JMenu noneLangSyntaxMenu;
123125
private final EditorPaneActions actions;
126+
private final VimHandler vim = new VimHandler(this);
124127

125128

126129
@Parameter
@@ -434,6 +437,24 @@ boolean isWordChar(final RTextArea textArea, final int pos) {
434437
};
435438
}
436439

440+
@Override
441+
protected void processKeyEvent(final KeyEvent e) {
442+
if (vim.processKeyEvent(e)) e.consume();
443+
else super.processKeyEvent(e);
444+
}
445+
446+
public VimHandler getVimHandler() {
447+
return vim;
448+
}
449+
450+
public boolean isVimEnabled() {
451+
return vim.isEnabled();
452+
}
453+
454+
public void setVimEnabled(final boolean enabled) {
455+
vim.setEnabled(enabled);
456+
}
457+
437458
@Override
438459
public void undoLastAction() {
439460
undoInProgress = true;
@@ -1046,6 +1067,7 @@ public void convertSpacesToTabs() {
10461067
public static final String AUTOCOMPLETE_KEYLESS_PREFS = "script.editor.ACNoKey";
10471068
public static final String AUTOCOMPLETE_FALLBACK_PREFS = "script.editor.ACFallback";
10481069
public static final String MARK_OCCURRENCES_PREFS = "script.editor.Occurrences";
1070+
public static final String VIM_PREFS = "script.editor.Vim";
10491071
public static final String FOLDERS_PREFS = "script.editor.folders";
10501072
public static final int DEFAULT_TAB_SIZE = 4;
10511073
public static final String DEFAULT_THEME = "default";
@@ -1076,6 +1098,7 @@ public void loadPreferences() {
10761098
setFallbackAutoCompletion(prefService.getBoolean(getClass(), AUTOCOMPLETE_FALLBACK_PREFS, false));
10771099
setMarkOccurrences(prefService.getBoolean(getClass(), MARK_OCCURRENCES_PREFS, false));
10781100
setMarginLineEnabled(prefService.getBoolean(getClass(), MARGIN_VISIBLE_PREFS, false));
1101+
setVimEnabled(prefService.getBoolean(getClass(), VIM_PREFS, false));
10791102
applyTheme(themeName());
10801103
}
10811104
}
@@ -1132,6 +1155,7 @@ public void savePreferences(final String top_folders, final String theme) {
11321155
prefService.put(getClass(), AUTOCOMPLETE_FALLBACK_PREFS, isAutoCompletionFallbackEnabled());
11331156
prefService.put(getClass(), MARGIN_VISIBLE_PREFS, isMarginLineEnabled());
11341157
prefService.put(getClass(), MARK_OCCURRENCES_PREFS, getMarkOccurrences());
1158+
prefService.put(getClass(), VIM_PREFS, isVimEnabled());
11351159
if (null != top_folders) prefService.put(getClass(), FOLDERS_PREFS, top_folders);
11361160
if (null != theme) prefService.put(getClass(), THEME_PREFS, theme);
11371161
}

‎src/main/java/org/scijava/ui/swing/script/TextEditor.java‎

Lines changed: 59 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,7 @@ public class TextEditor extends JFrame implements ActionListener,
230230
private FindAndReplaceDialog findDialog;
231231
private JCheckBoxMenuItem autoSave, wrapLines, tabsEmulated, autoImport,
232232
autocompletion, fallbackAutocompletion, keylessAutocompletion,
233-
markOccurences, paintTabs, whiteSpace, marginLine, lockPane;
233+
markOccurences, paintTabs, whiteSpace, marginLine, lockPane, vimMode;
234234
private ButtonGroup themeRadioGroup;
235235
private JTextArea errorScreen = new ConsoleArea(ConsoleArea.Content.ERRORS);
236236

@@ -648,6 +648,9 @@ public TextEditor(final Context context) {
648648
options.add(keylessAutocompletion);
649649
options.add(fallbackAutocompletion);
650650

651+
GuiUtils.addMenubarSeparator(options, "Key Bindings:");
652+
options.add(vimMode);
653+
651654
options.addSeparator();
652655
appendPreferences(options);
653656
mbar.add(options);
@@ -1130,6 +1133,10 @@ private void initializeDynamicMenuComponents() {
11301133
fallbackAutocompletion.setToolTipText("<HTML>If selected, Java completions will be used when scripting<br>"
11311134
+ "a language for which auto-completions are not available");
11321135
fallbackAutocompletion.addItemListener(e -> setFallbackAutoCompletion(fallbackAutocompletion.getState()));
1136+
vimMode = new JCheckBoxMenuItem("Vim Mode", false);
1137+
vimMode.setToolTipText("<HTML>Edit with vim-style modal key bindings.<br>"
1138+
+ "Press Esc for normal mode; :w saves, :q closes the tab");
1139+
vimMode.addItemListener(e -> setVimMode(vimMode.getState()));
11331140
themeRadioGroup = new ButtonGroup();
11341141

11351142
// Help menu. These are 'dynamic' items
@@ -1647,15 +1654,7 @@ else if (source == previousError) {
16471654
getEditorPane().getErrorHighlighter().gotoPreviousError();
16481655
}
16491656
else if (source == kill) chooseTaskToKill();
1650-
else if (source == close) if (tabbed.getTabCount() < 2) processWindowEvent(new WindowEvent(
1651-
this, WindowEvent.WINDOW_CLOSING));
1652-
else {
1653-
if (!handleUnsavedChanges()) return;
1654-
int index = tabbed.getSelectedIndex();
1655-
removeTab(index);
1656-
if (index > 0) index--;
1657-
switchTo(index);
1658-
}
1657+
else if (source == close) closeTab(true);
16591658
else if (source == copy) getTextArea().copy();
16601659
else if (source == find) findOrReplace(true);
16611660
else if (source == findNext) {
@@ -1812,6 +1811,55 @@ private void setMarginLineEnabled(final boolean enabled) {
18121811
getEditorPane().requestFocusInWindow();
18131812
}
18141813

1814+
/**
1815+
* Closes the current tab, or the window if it is the last tab.
1816+
*
1817+
* @param confirm Whether to ask about unsaved changes before closing a tab.
1818+
*/
1819+
private void closeTab(final boolean confirm) {
1820+
if (tabbed.getTabCount() < 2) {
1821+
processWindowEvent(new WindowEvent(this, WindowEvent.WINDOW_CLOSING));
1822+
return;
1823+
}
1824+
if (confirm && !handleUnsavedChanges()) return;
1825+
int index = tabbed.getSelectedIndex();
1826+
removeTab(index);
1827+
if (index > 0) index--;
1828+
switchTo(index);
1829+
}
1830+
1831+
/**
1832+
* Handles a vim ex command (typed after ':') which the editor pane does not
1833+
* handle itself.
1834+
*
1835+
* @return false if the command is unknown.
1836+
*/
1837+
boolean handleVimCommand(final String command) {
1838+
switch (command) {
1839+
case "w":
1840+
save();
1841+
return true;
1842+
case "q":
1843+
closeTab(true);
1844+
return true;
1845+
case "q!":
1846+
closeTab(false);
1847+
return true;
1848+
case "wq":
1849+
case "x":
1850+
if (save()) closeTab(true);
1851+
return true;
1852+
default:
1853+
return false;
1854+
}
1855+
}
1856+
1857+
private void setVimMode(final boolean enabled) {
1858+
for (int i = 0; i < tabbed.getTabCount(); i++)
1859+
getEditorPane(i).setVimEnabled(enabled);
1860+
getEditorPane().requestFocusInWindow();
1861+
}
1862+
18151863
private JMenu applyThemeMenu() {
18161864
final LinkedHashMap<String, String> map = new LinkedHashMap<>();
18171865
map.put("Default", "default");
@@ -2487,6 +2535,7 @@ else if (tabSize == Integer.parseInt(item.getText())) {
24872535
}
24882536
}
24892537
markOccurences.setState(pane.getMarkOccurrences());
2538+
vimMode.setState(pane.isVimEnabled());
24902539
wrapLines.setState(pane.getLineWrap());
24912540
marginLine.setState(pane.isMarginLineEnabled());
24922541
tabsEmulated.setState(pane.getTabsEmulated());

‎src/main/java/org/scijava/ui/swing/script/TextEditorTab.java‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import java.awt.BorderLayout;
3333
import java.awt.Color;
3434
import java.awt.Dimension;
35+
import java.awt.Font;
3536
import java.awt.GridBagConstraints;
3637
import java.awt.GridBagLayout;
3738
import java.awt.datatransfer.DataFlavor;
@@ -47,6 +48,7 @@
4748
import java.io.File;
4849
import java.util.List;
4950

51+
import javax.swing.BorderFactory;
5052
import javax.swing.JButton;
5153
import javax.swing.JCheckBox;
5254
import javax.swing.JComboBox;
@@ -98,6 +100,8 @@ public String toString() {
98100
private final ConsoleArea combinedScreen = new ConsoleArea(ConsoleArea.Content.MIXED);
99101
protected final JTextArea prompt = new JTextArea();
100102
private final JLabel prompt_title = new JLabel();
103+
/** Status line for vim mode: the mode, partial command or message. */
104+
private final JLabel vimStatus = new JLabel(" ");
101105
protected final JCheckBox updownarrows = new JCheckBox("Use arrow keys");
102106
protected final JScrollPane scroll;
103107
/** True iff the console is showing errors only. */
@@ -362,6 +366,16 @@ public void actionPerformed(final ActionEvent e) {
362366
final JPanel holder = new JPanel(new BorderLayout());
363367
holder.add(editorPane.wrappedInScrollbars());
364368
holder.add(es, BorderLayout.LINE_END);
369+
holder.add(vimStatus, BorderLayout.PAGE_END);
370+
vimStatus.setFont(new Font(Font.MONOSPACED, Font.PLAIN, vimStatus.getFont().getSize()));
371+
vimStatus.setBorder(BorderFactory.createEmptyBorder(2, 4, 2, 4));
372+
vimStatus.setVisible(false);
373+
editorPane.getVimHandler().setStatusListener(status -> {
374+
vimStatus.setVisible(status != null);
375+
// Note: a blank keeps the line from collapsing.
376+
vimStatus.setText(status == null || status.isEmpty() ? " " : status);
377+
});
378+
editorPane.getVimHandler().setExHandler(textEditor::handleVimCommand);
365379
super.setLeftComponent(holder);
366380
super.setRightComponent(screenAndPromptSplit);
367381
screenAndPromptSplit.setDividerLocation(1.0);

0 commit comments

Comments
 (0)