@@ -349,6 +349,29 @@ public void windowGainedFocus(WindowEvent e) {
349349
350350 // Enable window resizing (which allows for full screen button)
351351 setResizable (true );
352+
353+ {
354+ // Move Lines Keyboard Shortcut (Alt + Arrow Up/Down)
355+ KeyStroke moveUpKeyStroke = KeyStroke .getKeyStroke (KeyEvent .VK_UP , InputEvent .ALT_DOWN_MASK );
356+ final String MOVE_UP_ACTION_KEY = "moveLinesUp" ;
357+ textarea .getInputMap (JComponent .WHEN_FOCUSED ).put (moveUpKeyStroke , MOVE_UP_ACTION_KEY );
358+ textarea .getActionMap ().put (MOVE_UP_ACTION_KEY , new AbstractAction () {
359+ @ Override
360+ public void actionPerformed (ActionEvent e ) {
361+ handleMoveLines (true );
362+ }
363+ });
364+
365+ KeyStroke moveDownKeyStroke = KeyStroke .getKeyStroke (KeyEvent .VK_DOWN , InputEvent .ALT_DOWN_MASK );
366+ final String MOVE_DOWN_ACTION_KEY = "moveLinesDown" ;
367+ textarea .getInputMap (JComponent .WHEN_FOCUSED ).put (moveDownKeyStroke , MOVE_DOWN_ACTION_KEY );
368+ textarea .getActionMap ().put (MOVE_DOWN_ACTION_KEY , new AbstractAction () {
369+ @ Override
370+ public void actionPerformed (ActionEvent e ) {
371+ handleMoveLines (false );
372+ }
373+ });
374+ }
352375 }
353376
354377
@@ -1919,6 +1942,110 @@ public void handleIndentOutdent(boolean indent) {
19191942 sketch .setModified (true );
19201943 }
19211944
1945+
1946+ /**
1947+ * Moves the selected lines up or down in the text editor.
1948+ *
1949+ * <p>If {@code moveUp} is true, the selected lines are moved up. If false, they move down.</p>
1950+ * <p>This method ensures proper selection updates and handles edge cases like moving
1951+ * the first or last line.</p>
1952+ * <p>This operation is undo/redoable, allowing the user to revert the action using
1953+ * {@code Ctrl/Cmd + Z} (Undo). Redo functionality is available through the
1954+ * keybinding {@code Ctrl/Cmd + Z} on Windows/Linux and {@code Shift + Cmd + Z} on macOS.</p>
1955+ *
1956+ * @param moveUp {@code true} to move the selection up, {@code false} to move it down.
1957+ */
1958+ public void handleMoveLines (boolean moveUp ) {
1959+ startCompoundEdit ();
1960+ boolean isSelected = false ;
1961+
1962+ if (textarea .isSelectionActive ())
1963+ isSelected = true ;
1964+
1965+ int caretPos = textarea .getCaretPosition ();
1966+ int currentLine = textarea .getCaretLine ();
1967+ int lineStart = textarea .getLineStartOffset (currentLine );
1968+ int column = caretPos - lineStart ;
1969+
1970+ int startLine = textarea .getSelectionStartLine ();
1971+ int stopLine = textarea .getSelectionStopLine ();
1972+
1973+ // Adjust selection if the last line isn't fully selected
1974+ if (startLine != stopLine &&
1975+ textarea .getSelectionStop () == textarea .getLineStartOffset (stopLine )) {
1976+ stopLine --;
1977+ }
1978+
1979+ int replacedLine = moveUp ? startLine - 1 : stopLine + 1 ;
1980+ if (replacedLine < 0 || replacedLine >= textarea .getLineCount ()) {
1981+ stopCompoundEdit ();
1982+ return ;
1983+ }
1984+
1985+ final String source = textarea .getText (); // Get full text from textarea
1986+
1987+ int replaceStart = textarea .getLineStartOffset (replacedLine );
1988+ int replaceEnd = textarea .getLineStopOffset (replacedLine );
1989+ if (replaceEnd > source .length ()) {
1990+ replaceEnd = source .length ();
1991+ }
1992+
1993+ int selectionStart = textarea .getLineStartOffset (startLine );
1994+ int selectionEnd = textarea .getLineStopOffset (stopLine );
1995+ if (selectionEnd > source .length ()) {
1996+ selectionEnd = source .length ();
1997+ }
1998+
1999+ String replacedText = source .substring (replaceStart , replaceEnd );
2000+ String selectedText = source .substring (selectionStart , selectionEnd );
2001+
2002+ if (replacedLine == textarea .getLineCount () - 1 ) {
2003+ replacedText += "\n " ;
2004+ selectedText = selectedText .substring (0 , Math .max (0 , selectedText .length () - 1 ));
2005+ } else if (stopLine == textarea .getLineCount () - 1 ) {
2006+ selectedText += "\n " ;
2007+ replacedText = replacedText .substring (0 , Math .max (0 , replacedText .length () - 1 ));
2008+ }
2009+
2010+ int newSelectionStart , newSelectionEnd ;
2011+ if (moveUp ) {
2012+ textarea .select (selectionStart , selectionEnd );
2013+ textarea .setSelectedText (replacedText ); // Use setSelectedText()
2014+
2015+ textarea .select (replaceStart , replaceEnd );
2016+ textarea .setSelectedText (selectedText );
2017+
2018+ newSelectionStart = textarea .getLineStartOffset (startLine - 1 );
2019+ newSelectionEnd = textarea .getLineStopOffset (stopLine - 1 );
2020+ } else {
2021+ textarea .select (replaceStart , replaceEnd );
2022+ textarea .setSelectedText (selectedText );
2023+
2024+ textarea .select (selectionStart , selectionEnd );
2025+ textarea .setSelectedText (replacedText );
2026+
2027+ newSelectionStart = textarea .getLineStartOffset (startLine + 1 );
2028+ newSelectionEnd = stopLine + 1 < textarea .getLineCount ()
2029+ ? Math .min (textarea .getLineStopOffset (stopLine + 1 ), source .length ())
2030+ : textarea .getLineStopOffset (stopLine ); // Prevent out-of-bounds
2031+ }
2032+ stopCompoundEdit ();
2033+
2034+ if (isSelected )
2035+ SwingUtilities .invokeLater (() -> {
2036+ textarea .select (newSelectionStart , newSelectionEnd -1 );
2037+ });
2038+ else if (replacedLine >= 0 && replacedLine < textarea .getLineCount ()) {
2039+ int replacedLineStart = textarea .getLineStartOffset (replacedLine );
2040+ int replacedLineEnd = textarea .getLineStopOffset (replacedLine );
2041+
2042+ // Ensure caret stays within bounds of the new line
2043+ int newCaretPos = Math .min (replacedLineStart + column , replacedLineEnd - 1 );
2044+
2045+ SwingUtilities .invokeLater (() -> textarea .setCaretPosition (newCaretPos ));
2046+ }
2047+ }
2048+
19222049
19232050 static public boolean checkParen (char [] array , int index , int stop ) {
19242051 while (index < stop ) {
0 commit comments