blob: 891fd2d467586b08337ab6f1b189efd331b211dd [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package groovy.console.ui
import groovy.cli.internal.CliBuilderInternal
import groovy.cli.internal.OptionAccessor
import groovy.console.ui.text.FindReplaceUtility
import groovy.console.ui.text.GroovyFilter
import groovy.console.ui.text.SmartDocumentFilter
import groovy.swing.SwingBuilder
import groovy.transform.CompileStatic
import groovy.transform.ThreadInterrupt
import groovy.ui.GroovyMain
import org.apache.groovy.antlr.LexerFrame
import org.apache.groovy.io.StringBuilderWriter
import org.apache.groovy.util.SystemUtil
import org.codehaus.groovy.control.CompilerConfiguration
import org.codehaus.groovy.control.ErrorCollector
import org.codehaus.groovy.control.MultipleCompilationErrorsException
import org.codehaus.groovy.control.customizers.ASTTransformationCustomizer
import org.codehaus.groovy.control.messages.ExceptionMessage
import org.codehaus.groovy.control.messages.SimpleMessage
import org.codehaus.groovy.control.messages.SyntaxErrorMessage
import org.codehaus.groovy.runtime.StackTraceUtils
import org.codehaus.groovy.runtime.StringGroovyMethods
import org.codehaus.groovy.syntax.SyntaxException
import org.codehaus.groovy.tools.shell.util.MessageSource
import org.codehaus.groovy.transform.ThreadInterruptibleASTTransformation
import javax.swing.Action
import javax.swing.Icon
import javax.swing.JApplet
import javax.swing.JFileChooser
import javax.swing.JFrame
import javax.swing.JLabel
import javax.swing.JOptionPane
import javax.swing.JScrollPane
import javax.swing.JSplitPane
import javax.swing.JTextPane
import javax.swing.RootPaneContainer
import javax.swing.SwingUtilities
import javax.swing.Timer
import javax.swing.UIManager
import javax.swing.event.CaretEvent
import javax.swing.event.CaretListener
import javax.swing.event.DocumentListener
import javax.swing.event.HyperlinkEvent
import javax.swing.event.HyperlinkListener
import javax.swing.filechooser.FileFilter
import javax.swing.text.AttributeSet
import javax.swing.text.Document
import javax.swing.text.Element
import javax.swing.text.SimpleAttributeSet
import javax.swing.text.Style
import javax.swing.text.StyleConstants
import javax.swing.text.html.HTML
import java.awt.BorderLayout
import java.awt.Component
import java.awt.Dimension
import java.awt.EventQueue
import java.awt.Font
import java.awt.Toolkit
import java.awt.Window
import java.awt.event.ActionEvent
import java.awt.event.ComponentEvent
import java.awt.event.ComponentListener
import java.awt.event.FocusEvent
import java.awt.event.FocusListener
import java.util.logging.Logger
import java.util.prefs.Preferences
/**
* Groovy Swing console.
*
* Allows user to interactively enter and execute Groovy.
*/
class Console implements CaretListener, HyperlinkListener, ComponentListener, FocusListener {
static final String DEFAULT_SCRIPT_NAME_START = 'ConsoleScript'
static private prefs = Preferences.userNodeForPackage(Console)
// Whether or not std output should be captured to the console
static boolean captureStdOut = prefs.getBoolean('captureStdOut', true)
static boolean captureStdErr = prefs.getBoolean('captureStdErr', true)
static consoleControllers = []
static boolean smartHighlighter = prefs.getBoolean('smartHighlighter',
Boolean.valueOf(SystemUtil.getSystemPropertySafe('groovy.console.enable.smart.highlighter', 'true')))
boolean fullStackTraces = prefs.getBoolean('fullStackTraces',
Boolean.valueOf(System.getProperty('groovy.full.stacktrace', 'false')))
Action fullStackTracesAction
boolean showScriptInOutput = prefs.getBoolean('showScriptInOutput', true)
Action showScriptInOutputAction
boolean visualizeScriptResults = prefs.getBoolean('visualizeScriptResults', false)
Action visualizeScriptResultsAction
boolean showToolbar = prefs.getBoolean('showToolbar', true)
Component toolbar
Action showToolbarAction
boolean detachedOutput = prefs.getBoolean('detachedOutput', false)
Action detachedOutputAction
boolean orientationVertical = prefs.getBoolean('orientationVertical', true)
Action orientationVerticalAction
Action showOutputWindowAction
Action hideOutputWindowAction1
Action hideOutputWindowAction2
Action hideOutputWindowAction3
Action hideOutputWindowAction4
int origDividerSize
Component outputWindow
Component copyFromComponent
Component blank
Component scrollArea
boolean autoClearOutput = prefs.getBoolean('autoClearOutput', false)
Action autoClearOutputAction
// Safer thread interruption
boolean threadInterrupt = prefs.getBoolean('threadInterrupt', false)
Action threadInterruptAction
Action saveOnRunAction
boolean saveOnRun = prefs.getBoolean('saveOnRun', false)
Action loopModeAction
boolean loopMode = prefs.getBoolean('loopMode', false)
int inputAreaContentHash
boolean indy = prefs.getBoolean('indy', false)
Action indyAction
//to allow loading classes dynamically when using @Grab (GROOVY-4877, GROOVY-5871)
boolean useScriptClassLoaderForScriptExecution = false
// Maximum size of history
int maxHistory = 10
// Maximum number of characters to show on console at any time
int maxOutputChars = System.getProperty('groovy.console.output.limit', '20000') as int
// File to output stdout & stderr, in addition to console
PrintWriter outputPrintWriter = null
// UI
SwingBuilder swing
RootPaneContainer frame
ConsoleTextEditor inputEditor
JSplitPane splitPane
JTextPane inputArea
JTextPane outputArea
JLabel statusLabel
JLabel rowNumAndColNum
// row info
Element rootElement
int cursorPos
int rowNum
int colNum
// Styles for output area
Style promptStyle
Style commandStyle
Style outputStyle
Style stacktraceStyle
Style hyperlinkStyle
Style resultStyle
// Internal history
List history = []
int historyIndex = 1 // valid values are 0..history.length()
HistoryRecord pendingRecord = new HistoryRecord(allText: '', selectionStart: 0, selectionEnd: 0)
Action prevHistoryAction
Action nextHistoryAction
// Current editor state
boolean dirty
Action saveAction
int textSelectionStart // keep track of selections in inputArea
int textSelectionEnd
def scriptFile
File currentFileChooserDir = new File(Preferences.userNodeForPackage(Console).get('currentFileChooserDir', '.'))
File currentClasspathJarDir = new File(Preferences.userNodeForPackage(Console).get('currentClasspathJarDir', '.'))
File currentClasspathDir = new File(Preferences.userNodeForPackage(Console).get('currentClasspathDir', '.'))
// Running scripts
CompilerConfiguration baseConfig
CompilerConfiguration config
GroovyShell shell
int scriptNameCounter = 0
SystemOutputInterceptor systemOutInterceptor
SystemOutputInterceptor systemErrorInterceptor
Thread runThread = null
Closure beforeExecution
Closure afterExecution
public static URL ICON_PATH = Console.class.classLoader.getResource('groovy/console/ui/ConsoleIcon.png')
// used by ObjectBrowser and AST Viewer
public static URL NODE_ICON_PATH = Console.class.classLoader.getResource('groovy/console/ui/icons/bullet_green.png')
// used by AST Viewer
static groovyFileFilter = new GroovyFileFilter()
boolean scriptRunning = false
boolean stackOverFlowError = false
Action interruptAction
Action selectWordAction
Action selectPreviousWordAction
ConsolePreferences consolePreferences
static void main(args) {
MessageSource messages = new MessageSource(Console)
def cli = new CliBuilderInternal(usage: 'groovyConsole [options] [filename]', stopAtNonOption: false,
header: messages['cli.option.header'])
cli.with {
_(names: ['-cp', '-classpath', '--classpath'], messages['cli.option.classpath.description'])
h(longOpt: 'help', messages['cli.option.help.description'])
V(longOpt: 'version', messages['cli.option.version.description'])
pa(longOpt: 'parameters', messages['cli.option.parameters.description'])
i(longOpt: 'indy', messages['cli.option.indy.description'])
D(longOpt: 'define', type: Map, argName: 'name=value', messages['cli.option.define.description'])
_(longOpt: 'configscript', args: 1, messages['cli.option.configscript.description'])
}
OptionAccessor options = cli.parse(args)
if (options == null) {
// CliBuilder prints error, but does not exit
System.exit(22) // Invalid Args
}
if (options.h) {
cli.usage()
System.exit(0)
}
if (options.V) {
System.out.println(messages.format('cli.info.version', GroovySystem.version))
System.exit(0)
}
if (options.hasOption('D')) {
options.Ds.each { k, v -> System.setProperty(k, v) }
}
// full stack trace should not be logged to the output window - GROOVY-4663
Logger.getLogger(StackTraceUtils.STACK_LOG_NAME).useParentHandlers = false
//when starting via main set the look and feel to system
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName())
def baseConfig = new CompilerConfiguration(System.getProperties())
String starterConfigScripts = System.getProperty("groovy.starter.configscripts", null)
if (options.configscript || (starterConfigScripts != null && !starterConfigScripts.isEmpty())) {
List<String> configScripts = new ArrayList<String>()
if (options.configscript) {
configScripts.add(options.configscript)
}
if (starterConfigScripts != null) {
configScripts.addAll(StringGroovyMethods.tokenize((CharSequence) starterConfigScripts, ','))
}
GroovyMain.processConfigScripts(configScripts, baseConfig)
}
baseConfig.setParameters(options.hasOption("pa"))
if (options.i) {
enableIndy(baseConfig)
}
def console = new Console(Thread.currentThread().contextClassLoader, new Binding(), baseConfig)
console.useScriptClassLoaderForScriptExecution = true
console.run()
def remaining = options.arguments()
if (remaining && !remaining[-1].startsWith("-")) {
console.loadScriptFile(remaining[-1] as File)
}
}
int loadMaxOutputChars() {
// For backwards compatibility 'maxOutputChars' remains defined in the Console class
// and the System Property takes precedence as the default value.
int max = prefs.getInt('maxOutputChars', ConsolePreferences.DEFAULT_MAX_OUTPUT_CHARS)
return System.getProperty('groovy.console.output.limit', "${max}") as int
}
void preferences(EventObject evt = null) {
if (!consolePreferences) {
consolePreferences = new ConsolePreferences(this)
}
consolePreferences.show()
}
void setOutputPreferences(boolean useOutputFile, File outputFile) {
prefs.remove('outputLogFileName')
if (!useOutputFile) {
closeOutputPrintWriter(outputFile)
} else {
if (outputFile != null) {
closeOutputPrintWriter()
createOutputPrintWriter(outputFile)
prefs.put('outputLogFileName', outputFile.getAbsolutePath())
}
}
}
void createOutputPrintWriter(File outputFile) {
outputPrintWriter = new PrintWriter(new FileOutputStream(
outputFile,
true))
}
void closeOutputPrintWriter() {
if (outputPrintWriter != null) {
outputPrintWriter.close()
outputPrintWriter = null
}
}
Console(Binding binding = new Binding()) {
this(null, binding)
}
Console(ClassLoader parent, Binding binding = new Binding(), CompilerConfiguration baseConfig = new CompilerConfiguration(System.getProperties())) {
this.baseConfig = baseConfig
this.maxOutputChars = loadMaxOutputChars()
indy = indy || isIndyEnabled(baseConfig)
if (indy) {
enableIndy(baseConfig)
}
// Set up output file for stdout/stderr, if any
def outputLogFileName = prefs.get('outputLogFileName', null)
if (outputLogFileName) {
createOutputPrintWriter(new File(outputLogFileName))
}
newScript(parent, binding)
try {
System.setProperty('groovy.full.stacktrace', System.getProperty('groovy.full.stacktrace',
Boolean.toString(prefs.getBoolean('fullStackTraces', false))))
} catch (SecurityException se) {
fullStackTracesAction.enabled = false
}
consoleControllers += this
// listen for Ivy events if Ivy is on the Classpath
try {
if (Class.forName('org.apache.ivy.core.event.IvyListener')) {
def ivyPluginClass = Class.forName('groovy.console.ui.ConsoleIvyPlugin')
ivyPluginClass.newInstance().addListener(this)
}
} catch (ClassNotFoundException ignore) {
}
binding.variables._outputTransforms = OutputTransforms.loadOutputTransforms()
}
void newScript(ClassLoader parent, Binding binding) {
config = new CompilerConfiguration(baseConfig)
config.addCompilationCustomizers(*baseConfig.compilationCustomizers)
if (threadInterrupt) {
config.addCompilationCustomizers(new ASTTransformationCustomizer(ThreadInterrupt))
}
shell = new GroovyShell(parent, binding, config)
}
static frameConsoleDelegates = [
rootContainerDelegate: {
frame(
title: 'GroovyConsole',
//location: [100,100], // in groovy 2.0 use platform default location
iconImage: imageIcon('/groovy/console/ui/ConsoleIcon.png').image,
defaultCloseOperation: JFrame.DO_NOTHING_ON_CLOSE,
) {
try {
current.locationByPlatform = true
} catch (Exception e) {
current.location = [100, 100] // for 1.4 compatibility
}
containingWindows += current
}
},
menuBarDelegate : { arg ->
current.JMenuBar = build(arg)
}
]
void run() {
run(frameConsoleDelegates)
}
@Deprecated
void run(JApplet applet) {
run([
rootContainerDelegate: {
containingWindows += SwingUtilities.getRoot(applet.getParent())
applet
},
menuBarDelegate : { arg ->
current.JMenuBar = build(arg)
}
])
}
void run(Map defaults) {
swing = new SwingBuilder()
defaults.each { k, v -> swing[k] = v }
// tweak what the stack traces filter out to be fairly broad
System.setProperty('groovy.sanitized.stacktraces', '''org.codehaus.groovy.runtime.
org.codehaus.groovy.
org.apache.groovy.
groovy.lang.
gjdk.groovy.lang.
sun.
java.lang.reflect.
java.lang.Thread
groovy.console.ui.Console''')
// add controller to the swingBuilder bindings
swing.controller = this
// create the actions
swing.build(ConsoleActions)
// create the view
swing.build(ConsoleView)
bindResults()
// stitch some actions together
swing.bind(source: swing.inputEditor.undoAction, sourceProperty: 'enabled', target: swing.undoAction, targetProperty: 'enabled')
swing.bind(source: swing.inputEditor.redoAction, sourceProperty: 'enabled', target: swing.redoAction, targetProperty: 'enabled')
if (swing.consoleFrame instanceof Window) {
nativeFullScreenForMac(swing.consoleFrame)
swing.consoleFrame.pack()
swing.consoleFrame.show()
}
installInterceptor()
updateTitle() // Title changes based on indy setting
swing.doLater inputArea.&requestFocus
}
/**
* Make the console frames capable of native fullscreen
* for Mac OS X Lion and beyond.
*
* @param frame the application window
*/
private void nativeFullScreenForMac(Window frame) {
if (System.getProperty('os.name').contains('Mac OS X')) {
new GroovyShell(new Binding([frame: frame])).evaluate('''
try {
com.apple.eawt.FullScreenUtilities.setWindowCanFullScreen(frame, true)
} catch (Throwable t) {
// simply ignore as full screen capability is not available
}
''')
}
}
void installInterceptor() {
systemOutInterceptor = new SystemOutputInterceptor(this.&notifySystemOut, true)
systemOutInterceptor.start()
systemErrorInterceptor = new SystemOutputInterceptor(this.&notifySystemErr, false)
systemErrorInterceptor.start()
}
void addToHistory(record) {
history.add(record)
// history.size here just retrieves method closure
if (history.size() > maxHistory) {
history.remove(0)
}
// history.size doesn't work here either
historyIndex = history.size()
updateHistoryActions()
}
// Ensure we don't have too much in console (takes too much memory)
private ensureNoDocLengthOverflow(doc) {
// if it is a case of stackOverFlowError, show the exception details from the front
// as there is no point in showing the repeating details at the back
int offset = stackOverFlowError ? maxOutputChars : 0
if (doc.length > maxOutputChars) {
doc.remove(offset, doc.length - maxOutputChars)
}
}
// Append a string to the output area
void appendOutput(String text, AttributeSet style) {
def doc = outputArea.styledDocument
insertString(doc, doc.length, text, style)
ensureNoDocLengthOverflow(doc)
}
void appendOutput(Window window, AttributeSet style) {
appendOutput(window.toString(), style)
}
void appendOutput(Object object, AttributeSet style) {
appendOutput(object.toString(), style)
}
void appendOutput(Component component, AttributeSet style) {
SimpleAttributeSet sas = new SimpleAttributeSet()
sas.addAttribute(StyleConstants.NameAttribute, 'component')
StyleConstants.setComponent(sas, component)
appendOutput(component.toString(), sas)
}
void appendOutput(Icon icon, AttributeSet style) {
SimpleAttributeSet sas = new SimpleAttributeSet()
sas.addAttribute(StyleConstants.NameAttribute, 'icon')
StyleConstants.setIcon(sas, icon)
appendOutput(icon.toString(), sas)
}
void appendStacktrace(text) {
def doc = outputArea.styledDocument
// split lines by new line separator
def lines = text.split(/(\n|\r|\r\n|\u0085|\u2028|\u2029)/)
// Java Identifier regex
def ji = /([\p{Alnum}_\$][\p{Alnum}_\$]*)/
// stacktrace line regex
def stacktracePattern = /\tat $ji(\.$ji)+\((($ji(\.(java|groovy))?):(\d+))\)/
lines.each { line ->
int initialLength = doc.length
def matcher = line =~ stacktracePattern
def fileName = matcher.matches() ? matcher[0][-5] : ''
if (fileName == scriptFile?.name || fileName.startsWith(DEFAULT_SCRIPT_NAME_START)) {
def fileNameAndLineNumber = matcher[0][-6]
def length = fileNameAndLineNumber.length()
def index = line.indexOf(fileNameAndLineNumber)
def style = hyperlinkStyle
def hrefAttr = new SimpleAttributeSet()
// don't pass a GString as it won't be coerced to String as addAttribute takes an Object
hrefAttr.addAttribute(HTML.Attribute.HREF, 'file://' + fileNameAndLineNumber)
style.addAttribute(HTML.Tag.A, hrefAttr)
insertString(doc, initialLength, line[0..<index], stacktraceStyle)
insertString(doc, initialLength + index, line[index..<(index + length)], style)
insertString(doc, initialLength + index + length, line[(index + length)..-1] + '\n', stacktraceStyle)
} else {
insertString(doc, initialLength, line + '\n', stacktraceStyle)
}
}
ensureNoDocLengthOverflow(doc)
}
void insertString(Document doc, int offset, String text, AttributeSet attributeSet, boolean outputToFile = true) {
doc.insertString(offset, text, attributeSet)
// Output to file if activated
if (outputToFile && outputPrintWriter != null) {
outputPrintWriter.append(text)
outputPrintWriter.flush()
}
}
// Append a string to the output area on a new line
void appendOutputNl(text, style) {
def doc = outputArea.styledDocument
def len = doc.length
def alreadyNewLine = (len == 0 || doc.getText(len - 1, 1) == '\n')
insertString(doc, doc.length, ' \n', style)
if (alreadyNewLine) {
doc.remove(len, 2) // windows hack to fix (improve?) line spacing
}
appendOutput(text, style)
}
void appendOutputLines(text, style) {
appendOutput(text, style)
def doc = outputArea.styledDocument
def len = doc.length
// Disable output to log file in this case ('\n' is removed from outputArea next line)
insertString(doc, len, ' \n', style, false)
doc.remove(len, 2) // windows hack to fix (improve?) line spacing
}
// Return false if use elected to cancel
boolean askToSaveFile() {
if (!dirty) {
return true
}
switch (JOptionPane.showConfirmDialog(frame,
'Save changes' + (scriptFile != null ? " to ${scriptFile.name}" : '') + '?',
'GroovyConsole', JOptionPane.YES_NO_CANCEL_OPTION)) {
case JOptionPane.YES_OPTION:
return fileSave()
case JOptionPane.NO_OPTION:
return true
default:
return false
}
}
void beep() {
Toolkit.defaultToolkit.beep()
}
// Binds the '_' and '__' variables in the shell
void bindResults() {
shell.setVariable('_', getLastResult()) // lastResult doesn't seem to work
shell.setVariable('__', history.collect { it.result })
}
// Handles menu event
static void captureStdOut(EventObject evt) {
captureStdOut = evt.source.selected
prefs.putBoolean('captureStdOut', captureStdOut)
}
static void captureStdErr(EventObject evt) {
captureStdErr = evt.source.selected
prefs.putBoolean('captureStdErr', captureStdErr)
}
void fullStackTraces(EventObject evt) {
fullStackTraces = evt.source.selected
System.setProperty('groovy.full.stacktrace',
Boolean.toString(fullStackTraces))
prefs.putBoolean('fullStackTraces', fullStackTraces)
}
void showScriptInOutput(EventObject evt) {
showScriptInOutput = evt.source.selected
prefs.putBoolean('showScriptInOutput', showScriptInOutput)
}
void visualizeScriptResults(EventObject evt) {
visualizeScriptResults = evt.source.selected
prefs.putBoolean('visualizeScriptResults', visualizeScriptResults)
}
void showToolbar(EventObject evt) {
showToolbar = evt.source.selected
prefs.putBoolean('showToolbar', showToolbar)
toolbar.visible = showToolbar
}
void orientationVertical(EventObject evt) {
def oldValue = orientationVertical
orientationVertical = evt.source.selected
prefs.putBoolean('orientationVertical', orientationVertical)
if (oldValue != orientationVertical) {
if (orientationVertical) {
splitPane.setOrientation(JSplitPane.VERTICAL_SPLIT)
} else {
splitPane.setOrientation(JSplitPane.HORIZONTAL_SPLIT)
}
splitPane.resizeWeight = detachedOutput ? 1.0 : 0.5
splitPane.resetToPreferredSizes()
}
}
void detachedOutput(EventObject evt) {
def oldDetachedOutput = detachedOutput
detachedOutput = evt.source.selected
prefs.putBoolean('detachedOutput', detachedOutput)
if (oldDetachedOutput != detachedOutput) {
if (detachedOutput) {
splitPane.add(blank, JSplitPane.BOTTOM)
origDividerSize = splitPane.dividerSize
splitPane.dividerSize = 0
splitPane.resizeWeight = 1.0
outputWindow.add(scrollArea, BorderLayout.CENTER)
prepareOutputWindow()
} else {
splitPane.add(scrollArea, JSplitPane.BOTTOM)
splitPane.dividerSize = origDividerSize
outputWindow.add(blank, BorderLayout.CENTER)
outputWindow.visible = false
splitPane.resizeWeight = 0.5
}
}
}
void autoClearOutput(EventObject evt) {
autoClearOutput = evt.source.selected
prefs.putBoolean('autoClearOutput', autoClearOutput)
}
void threadInterruption(EventObject evt) {
threadInterrupt = evt.source.selected
prefs.putBoolean('threadInterrupt', threadInterrupt)
def customizers = config.compilationCustomizers.iterator()
while (customizers.hasNext()) {
def next = customizers.next()
if (next instanceof ASTTransformationCustomizer) {
ASTTransformationCustomizer astCustomizer = next
if (astCustomizer.transformation instanceof ThreadInterruptibleASTTransformation) {
customizers.remove()
}
}
}
if (threadInterrupt) {
config.addCompilationCustomizers(new ASTTransformationCustomizer(ThreadInterrupt))
}
}
void caretUpdate(CaretEvent e) {
textSelectionStart = Math.min(dot(e), mark(e))
textSelectionEnd = Math.max(dot(e), mark(e))
setRowNumAndColNum()
}
// GROOVY-8339: to avoid illegal access to a non-visible implementation class - can be removed if a more general solution is found
@CompileStatic
int dot(CaretEvent e) {
e.dot
}
// GROOVY-8339: to avoid illegal access to a non-visible implementation class - can be removed if a more general solution is found
@CompileStatic
int mark(CaretEvent e) {
e.mark
}
void clearOutput(EventObject evt = null) {
outputArea.text = ''
}
// If at exit time, a script is running, the user is given an option to interrupt it first
def askToInterruptScript() {
if (!scriptRunning) return true
def rc = JOptionPane.showConfirmDialog(frame, "Script executing. Press 'OK' to attempt to interrupt it before exiting.",
'GroovyConsole', JOptionPane.OK_CANCEL_OPTION)
if (rc == JOptionPane.OK_OPTION) {
doInterrupt()
return true
} else {
return false
}
}
void doInterrupt(EventObject evt = null) {
runThread?.interrupt()
}
void exitDesktop(EventObject evt = null, quitResponse = null) {
exit(evt)
quitResponse.performQuit()
}
void exit(EventObject evt = null) {
if (askToInterruptScript()) {
if (askToSaveFile()) {
if (frame instanceof Window) {
frame.hide()
frame.dispose()
outputWindow?.dispose()
}
FindReplaceUtility.dispose()
consoleControllers.remove(this)
if (!consoleControllers) {
systemOutInterceptor.stop()
systemErrorInterceptor.stop()
}
}
}
}
void fileNewFile(EventObject evt = null) {
if (askToSaveFile()) {
scriptFile = null
setDirty(false)
inputArea.text = ''
}
}
// Start a new window with a copy of current variables
void fileNewWindow(EventObject evt = null) {
Console consoleController = new Console(
new Binding(
new HashMap(shell.getContext().variables)))
consoleController.systemOutInterceptor = systemOutInterceptor
consoleController.systemErrorInterceptor = systemErrorInterceptor
SwingBuilder swing = new SwingBuilder()
consoleController.swing = swing
frameConsoleDelegates.each { k, v -> swing[k] = v }
swing.controller = consoleController
swing.build(ConsoleActions)
swing.build(ConsoleView)
installInterceptor()
nativeFullScreenForMac(swing.consoleFrame)
swing.consoleFrame.pack()
swing.consoleFrame.show()
swing.doLater swing.inputArea.&requestFocus
}
void fileOpen(EventObject evt = null) {
if (askToSaveFile()) {
def scriptName = selectFilename()
if (scriptName != null) {
loadScriptFile(scriptName)
}
}
}
void loadScriptFile(File file) {
swing.edt {
inputArea.editable = false
}
swing.doOutside {
try {
consoleText = file.readLines().join('\n')
scriptFile = file
swing.edt {
def listeners = inputArea.document.getListeners(DocumentListener)
listeners.each { inputArea.document.removeDocumentListener(it) }
updateTitle()
inputArea.document.remove 0, inputArea.document.length
inputArea.document.insertString 0, consoleText, null
listeners.each { inputArea.document.addDocumentListener(it) }
setDirty(false)
inputArea.caretPosition = 0
}
} finally {
swing.edt { inputArea.editable = true }
// GROOVY-3684: focus away and then back to inputArea ensures caret blinks
swing.doLater outputArea.&requestFocusInWindow
swing.doLater inputArea.&requestFocusInWindow
}
}
}
// Save file - return false if user cancelled save
boolean fileSave(EventObject evt = null) {
if (scriptFile == null) {
return fileSaveAs(evt)
}
scriptFile.write(inputArea.text)
setDirty(false)
return true
}
// Save file - return false if user cancelled save
boolean fileSaveAs(EventObject evt = null) {
scriptFile = selectFilename('Save')
if (scriptFile != null) {
scriptFile.write(inputArea.text)
setDirty(false)
return true
} else {
return false
}
}
def finishException(Throwable t, boolean executing) {
if (executing) {
statusLabel.text = 'Execution terminated with exception.'
history[-1].exception = t
} else {
statusLabel.text = 'Compilation failed.'
}
if (t instanceof MultipleCompilationErrorsException) {
MultipleCompilationErrorsException mcee = t
ErrorCollector collector = mcee.errorCollector
int count = collector.errorCount
appendOutputNl("${count} compilation error${count > 1 ? 's' : ''}:\n\n", commandStyle)
collector.errors.each { error ->
if (error instanceof SyntaxErrorMessage) {
SyntaxException se = error.cause
int errorLine = se.line
String message = se.originalMessage
String scriptFileName = scriptFile?.name ?: DEFAULT_SCRIPT_NAME_START
def doc = outputArea.styledDocument
def style = hyperlinkStyle
def hrefAttr = new SimpleAttributeSet()
// don't pass a GString as it won't be coerced to String as addAttribute takes an Object
hrefAttr.addAttribute(HTML.Attribute.HREF, 'file://' + scriptFileName + ':' + errorLine)
style.addAttribute(HTML.Tag.A, hrefAttr)
insertString(doc, doc.length, message + ' at ', stacktraceStyle)
insertString(doc, doc.length, "line: ${se.line}, column: ${se.startColumn}\n\n", style)
} else if (error instanceof Throwable) {
reportException(error)
} else if (error instanceof ExceptionMessage) {
reportException(error.cause)
} else if (error instanceof SimpleMessage) {
def doc = outputArea.styledDocument
insertString(doc, doc.length, "${error.message}\n", new SimpleAttributeSet())
}
}
} else {
reportException(t)
}
if (!executing) {
bindResults()
}
// GROOVY-4496: set the output window position to the top-left so the exception details are visible from the start
outputArea.caretPosition = 0
if (detachedOutput) {
prepareOutputWindow()
showOutputWindow()
}
}
private calcPreferredSize(a, b, c) {
[c, [a, b].min()].max()
}
private reportException(Throwable t) {
appendOutputNl('Exception thrown\n', commandStyle)
Writer sw = new StringBuilderWriter()
new PrintWriter(sw).withWriter { pw -> StackTraceUtils.deepSanitize(t).printStackTrace(pw) }
appendStacktrace("\n${sw.builder}\n")
}
def finishNormal(Object result) {
// Take down the wait/cancel dialog
history[-1].result = result
if (result != null) {
statusLabel.text = 'Execution complete.'
appendOutputNl('Result: ', promptStyle)
def obj = (visualizeScriptResults
? OutputTransforms.transformResult(result, shell.getContext()._outputTransforms)
: result.toString())
// multi-methods are magical!
appendOutput(obj, resultStyle)
} else {
statusLabel.text = 'Execution complete. Result was null.'
}
bindResults()
if (detachedOutput) {
prepareOutputWindow()
showOutputWindow()
}
}
def compileFinishNormal() {
statusLabel.text = 'Compilation complete.'
}
private def prepareOutputWindow() {
outputArea.setPreferredSize(null)
outputWindow.pack()
outputArea.setPreferredSize([calcPreferredSize(outputWindow.getWidth(), inputEditor.getWidth(), 120),
calcPreferredSize(outputWindow.getHeight(), inputEditor.getHeight(), 60)] as Dimension)
outputWindow.pack()
}
// Gets the last, non-null result
def getLastResult() {
// runtime bugs in here history.reverse produces odd lookup
// return history.reverse.find {it != null}
if (!history) {
return
}
for (i in (history.size() - 1)..0) {
if (history[i].result != null) {
return history[i].result
}
}
return null
}
void historyNext(EventObject evt = null) {
if (historyIndex < history.size()) {
setInputTextFromHistory(historyIndex + 1)
} else {
statusLabel.text = "Can't go past end of history (time travel not allowed)"
beep()
}
}
void historyPrev(EventObject evt = null) {
if (historyIndex > 0) {
setInputTextFromHistory(historyIndex - 1)
} else {
statusLabel.text = "Can't go past start of history"
beep()
}
}
void inspectLast(EventObject evt = null) {
if (null == lastResult) {
JOptionPane.showMessageDialog(frame, 'The last result is null.',
'Cannot Inspect', JOptionPane.INFORMATION_MESSAGE)
return
}
ObjectBrowser.inspect(lastResult)
}
void inspectVariables(EventObject evt = null) {
ObjectBrowser.inspect(shell.getContext().variables)
}
void inspectAst(EventObject evt = null) {
new AstBrowser(inputArea, rootElement, shell.getClassLoader(), config).run({ inputArea.getText() })
}
void inspectTokens(EventObject evt = null) {
def content = inputArea.getText()
def lf = new LexerFrame(new StringReader(content))
lf.visible = true
}
void largerFont(EventObject evt = null) {
updateFontSize(inputArea.font.size + 2)
}
static boolean notifySystemOut(int consoleId, String str) {
if (!captureStdOut) {
// Output as normal
return true
}
Closure doAppend = {
Console console = findConsoleById(consoleId)
if (console) {
console.appendOutputLines(str, console.outputStyle)
} else {
consoleControllers.each { it.appendOutputLines(str, it.outputStyle) }
}
}
// Put onto GUI
if (EventQueue.isDispatchThread()) {
doAppend.call()
} else {
SwingUtilities.invokeLater doAppend
}
return false
}
static boolean notifySystemErr(int consoleId, String str) {
if (!captureStdErr) {
// Output as normal
return true
}
Closure doAppend = {
Console console = findConsoleById(consoleId)
if (console) {
console.appendStacktrace(str)
} else {
consoleControllers.each { it.appendStacktrace(str) }
}
}
// Put onto GUI
if (EventQueue.isDispatchThread()) {
doAppend.call()
} else {
SwingUtilities.invokeLater doAppend
}
return false
}
int getConsoleId() {
return System.identityHashCode(this)
}
private static Console findConsoleById(int consoleId) {
return consoleControllers.find { it.consoleId == consoleId }
}
// actually run the script
void runScript(EventObject evt = null) {
saveInputAreaContentHash()
if (saveOnRun && scriptFile != null) {
if (fileSave(evt)) runScriptImpl(false)
} else {
runScriptImpl(false)
}
}
void saveOnRun(EventObject evt = null) {
saveOnRun = evt.source.selected
prefs.putBoolean('saveOnRun', saveOnRun)
}
void loopMode(EventObject evt = null) {
loopMode = evt.source.selected
prefs.putBoolean('loopMode', loopMode)
}
void indy(EventObject evt = null) {
indy = evt.source.selected
prefs.putBoolean('indy', indy)
if (indy) {
enableIndy(baseConfig)
} else {
disableIndy(baseConfig)
}
updateTitle()
newScript(shell.classLoader, shell.context)
}
private static void enableIndy(CompilerConfiguration cc) {
cc.getOptimizationOptions().put(CompilerConfiguration.INVOKEDYNAMIC, true)
}
private static void disableIndy(CompilerConfiguration cc) {
cc.getOptimizationOptions().remove(CompilerConfiguration.INVOKEDYNAMIC)
}
private static boolean isIndyEnabled(CompilerConfiguration cc) {
cc.getOptimizationOptions().get(CompilerConfiguration.INVOKEDYNAMIC)
}
void runSelectedScript(EventObject evt = null) {
saveInputAreaContentHash()
runScriptImpl(true)
}
void addClasspathJar(EventObject evt = null) {
def fc = new JFileChooser(currentClasspathJarDir)
fc.fileSelectionMode = JFileChooser.FILES_ONLY
fc.multiSelectionEnabled = true
fc.acceptAllFileFilterUsed = true
if (fc.showDialog(frame, 'Add') == JFileChooser.APPROVE_OPTION) {
currentClasspathJarDir = fc.currentDirectory
Preferences.userNodeForPackage(Console).put('currentClasspathJarDir', currentClasspathJarDir.path)
fc.selectedFiles?.each { file ->
shell.getClassLoader().addURL(file.toURL())
}
}
}
void addClasspathDir(EventObject evt = null) {
def fc = new JFileChooser(currentClasspathDir)
fc.fileSelectionMode = JFileChooser.DIRECTORIES_ONLY
fc.acceptAllFileFilterUsed = true
if (fc.showDialog(frame, 'Add') == JFileChooser.APPROVE_OPTION) {
currentClasspathDir = fc.currentDirectory
Preferences.userNodeForPackage(Console).put('currentClasspathDir', currentClasspathDir.path)
shell.getClassLoader().addURL(fc.selectedFile.toURL())
}
}
void listClasspath(EventObject evt = null) {
List<URL> urls = []
ClassLoader cl = shell.classLoader
while (cl instanceof URLClassLoader) {
cl.getURLs().each { url -> urls << url }
cl = cl.parent
}
boolean isWin = isWindows()
List data = urls.unique().collect { url -> [name: new File(url.toURI()).name, path: isWin ? url.path.substring(1).replace('/', '\\') : url.path] }
data.sort { it.name.toLowerCase() }
JScrollPane scrollPane = swing.scrollPane {
table {
tableModel(list: data) {
propertyColumn(header: 'Name', propertyName: 'name', editable: false)
propertyColumn(header: ' Path', propertyName: 'path', editable: false)
}
}
}
def pane = swing.optionPane()
pane.message = scrollPane
def dialog = pane.createDialog(frame, 'Classpath')
dialog.setSize(800, 600)
dialog.resizable = true
dialog.visible = true
}
void clearContext(EventObject evt = null) {
def binding = new Binding()
newScript(null, binding)
// reload output transforms
binding.variables._outputTransforms = OutputTransforms.loadOutputTransforms()
}
private void saveInputAreaContentHash() {
inputAreaContentHash = inputArea.getText().hashCode()
}
private void runScriptImpl(boolean selected) {
if (scriptRunning) {
statusLabel.text = 'Cannot run script now as a script is already running. Please wait or use "Interrupt Script" option.'
return
}
scriptRunning = true
interruptAction.enabled = true
stackOverFlowError = false // reset this flag before running a script
def endLine = System.lineSeparator()
def record = new HistoryRecord(allText: inputArea.getText().replace(endLine, '\n'),
selectionStart: textSelectionStart, selectionEnd: textSelectionEnd)
addToHistory(record)
pendingRecord = new HistoryRecord(allText: '', selectionStart: 0, selectionEnd: 0)
if (prefs.getBoolean('autoClearOutput', false)) clearOutput()
// Print the input text
if (showScriptInOutput) {
for (line in record.getTextToRun(selected).tokenize('\n')) {
appendOutputNl('groovy> ', promptStyle)
appendOutput(line, commandStyle)
}
appendOutputNl(' \n', promptStyle)
}
// Kick off a new thread to do the evaluation
// Run in a thread outside of EDT, this method is usually called inside the EDT
runThread = Thread.start {
try {
systemOutInterceptor.setConsoleId(this.getConsoleId())
SwingUtilities.invokeLater { showExecutingMessage() }
String name = scriptFile?.name ?: (DEFAULT_SCRIPT_NAME_START + scriptNameCounter++)
if (beforeExecution) {
beforeExecution()
}
def result
if (useScriptClassLoaderForScriptExecution) {
ClassLoader savedThreadContextClassLoader = Thread.currentThread().contextClassLoader
try {
Thread.currentThread().contextClassLoader = shell.classLoader
result = shell.run(record.getTextToRun(selected), name, [])
}
finally {
Thread.currentThread().contextClassLoader = savedThreadContextClassLoader
}
} else {
result = shell.run(record.getTextToRun(selected), name, [])
}
if (afterExecution) {
afterExecution()
}
SwingUtilities.invokeLater { finishNormal(result) }
} catch (Throwable t) {
if (t instanceof StackOverflowError) {
// set the flag that will be used in printing exception details in output pane
stackOverFlowError = true
clearOutput()
}
SwingUtilities.invokeLater { finishException(t, true) }
} finally {
runThread = null
scriptRunning = false
interruptAction.enabled = false
systemOutInterceptor.removeConsoleId()
if( loopMode ) {
int delay = prefs.getInt('loopModeDelay', ConsolePreferences.DEFAULT_LOOP_MODE_DELAY_MILLIS)
Timer timer = new Timer(delay, {
if( inputAreaContentHash == inputArea.getText().hashCode() ) {
runScriptImpl(selected)
}
})
timer.repeats = false
timer.start()
}
}
}
}
void compileScript(EventObject evt = null) {
if (scriptRunning) {
statusLabel.text = 'Cannot compile script now as a script is already running. Please wait or use "Interrupt Script" option.'
return
}
stackOverFlowError = false // reset this flag before running a script
def endLine = System.lineSeparator()
def record = new HistoryRecord(allText: inputArea.getText().replace(endLine, '\n'),
selectionStart: textSelectionStart, selectionEnd: textSelectionEnd)
if (prefs.getBoolean('autoClearOutput', false)) clearOutput()
// Print the input text
if (showScriptInOutput) {
for (line in record.allText.tokenize('\n')) {
appendOutputNl('groovy> ', promptStyle)
appendOutput(line, commandStyle)
}
appendOutputNl(' \n', promptStyle)
}
// Kick off a new thread to do the compilation
// Run in a thread outside of EDT, this method is usually called inside the EDT
runThread = Thread.start {
try {
SwingUtilities.invokeLater { showCompilingMessage() }
shell.getClassLoader().parseClass(record.allText)
SwingUtilities.invokeLater { compileFinishNormal() }
} catch (Throwable t) {
SwingUtilities.invokeLater { finishException(t, false) }
} finally {
runThread = null
}
}
}
def selectFilename(name = 'Open') {
def fc = new JFileChooser(currentFileChooserDir)
fc.fileSelectionMode = JFileChooser.FILES_ONLY
fc.acceptAllFileFilterUsed = true
fc.fileFilter = groovyFileFilter
if (name == 'Save') {
fc.selectedFile = new File('*.groovy')
}
if (fc.showDialog(frame, name) == JFileChooser.APPROVE_OPTION) {
currentFileChooserDir = fc.currentDirectory
Preferences.userNodeForPackage(Console).put('currentFileChooserDir', currentFileChooserDir.path)
return fc.selectedFile
} else {
return null
}
}
void setDirty(boolean newDirty) {
//TODO when @BoundProperty is live, this should be handled via listeners
dirty = newDirty
saveAction.enabled = newDirty
updateTitle()
}
private void setInputTextFromHistory(newIndex) {
def endLine = System.lineSeparator()
if (historyIndex >= history.size()) {
pendingRecord = new HistoryRecord(allText: inputArea.getText().replace(endLine, '\n'),
selectionStart: textSelectionStart, selectionEnd: textSelectionEnd)
}
historyIndex = newIndex
def record
if (historyIndex < history.size()) {
record = history[historyIndex]
statusLabel.text = "command history ${history.size() - historyIndex}"
} else {
record = pendingRecord
statusLabel.text = 'at end of history'
}
inputArea.text = record.allText
inputArea.selectionStart = record.selectionStart
inputArea.selectionEnd = record.selectionEnd
setDirty(true) // Should calculate dirty flag properly (hash last saved/read text in each file)
updateHistoryActions()
}
private void updateHistoryActions() {
nextHistoryAction.enabled = historyIndex < history.size()
prevHistoryAction.enabled = historyIndex > 0
}
// Adds a variable to the binding
// Useful for adding variables before opening the console
void setVariable(String name, Object value) {
shell.getContext().setVariable(name, value)
}
void showAbout(EventObject evt = null) {
def version = GroovySystem.getVersion()
def pane = swing.optionPane()
// work around GROOVY-1048
pane.setMessage('Welcome to the Groovy Console for evaluating Groovy scripts\nVersion ' + version)
def dialog = pane.createDialog(frame, 'About GroovyConsole')
dialog.show()
}
void find(EventObject evt = null) {
FindReplaceUtility.showDialog()
}
void findNext(EventObject evt = null) {
FindReplaceUtility.FIND_ACTION.actionPerformed(evt)
}
void findPrevious(EventObject evt = null) {
def reverseEvt = new ActionEvent(
evt.getSource(), evt.getID(),
evt.getActionCommand(), evt.getWhen(),
ActionEvent.SHIFT_MASK) //reverse
FindReplaceUtility.FIND_ACTION.actionPerformed(reverseEvt)
}
void replace(EventObject evt = null) {
FindReplaceUtility.showDialog(true)
}
void comment(EventObject evt = null) {
def rootElement = inputArea.document.defaultRootElement
def cursorPos = inputArea.getCaretPosition()
int startRow = rootElement.getElementIndex(cursorPos)
int endRow = startRow
if (inputArea.getSelectedText()) {
def selectionStart = inputArea.getSelectionStart()
startRow = rootElement.getElementIndex(selectionStart)
def selectionEnd = inputArea.getSelectionEnd()
endRow = rootElement.getElementIndex(selectionEnd)
}
// If multiple commented lines intermix with uncommented lines, consider them uncommented
def allCommented = true
startRow.upto(endRow) { rowIndex ->
def rowElement = rootElement.getElement(rowIndex)
int startOffset = rowElement.getStartOffset()
int endOffset = rowElement.getEndOffset()
String rowText = inputArea.document.getText(startOffset, endOffset - startOffset)
if (rowText.trim().length() < 2 || !rowText.trim().substring(0, 2).equals("//")) {
allCommented = false
}
}
startRow.upto(endRow) { rowIndex ->
def rowElement = rootElement.getElement(rowIndex)
int startOffset = rowElement.getStartOffset()
int endOffset = rowElement.getEndOffset()
String rowText = inputArea.document.getText(startOffset, endOffset - startOffset)
if (allCommented) {
// Uncomment this line if it is already commented
int slashOffset = rowText.indexOf("//")
inputArea.document.remove(slashOffset + startOffset, 2)
} else {
// Add comment string in front of this line
inputArea.document.insertString(startOffset, "//", new SimpleAttributeSet())
}
}
}
void selectBlock(EventObject evt = null) {
final int startPos = inputArea.getSelectionStart()
final int endPos = inputArea.getSelectionEnd()
final int startRow = rootElement.getElementIndex(startPos)
final int endRow = rootElement.getElementIndex(endPos)
final Element rowElement = rootElement.getElement(startRow)
final int startRowOffset = rowElement.getStartOffset()
final int endRowOffset = rowElement.getEndOffset()
// Empty line, nothing to do
if (startRowOffset == endRowOffset - 1) {
return
}
// Nothing is currently selected so select next chunk unless we are at the end of
// the line then we select the previous
if (startPos == endPos && selectWordAction != null && selectPreviousWordAction != null) {
if (endPos == endRowOffset - 1) {
selectPreviousWordAction.actionPerformed(evt)
} else {
selectWordAction.actionPerformed(evt)
}
return
}
// Partial selection on a single line but not the entire line or word
// selection actions are not available so select the entire line
if (startRow == endRow && (startPos != startRowOffset || (endPos != endRowOffset - 1))) {
inputArea.setSelectionStart(startRowOffset)
inputArea.setSelectionEnd(endRowOffset - 1)
return
}
// At this point an entire line or multiple lines are selected so
// look for a block/paragraph to select
String rowText = inputArea.document.getText(startRowOffset, endRowOffset - startRowOffset)
if (!rowText?.trim()) {
// Selection is empty or all spaces so not part of any block
return
}
// Look up for first empty row
int startBlockPos = startRowOffset
for (int i = startRow - 1; i >= 0; i--) {
Element re = rootElement.getElement(i)
rowText = inputArea.document.getText(re.getStartOffset(), re.getEndOffset() - re.getStartOffset())
if (!rowText?.trim()) {
break
}
startBlockPos = re.getStartOffset()
}
// Look down for first empty row
int endBlockPos = endRowOffset
int totalRows = rootElement.getElementCount()
for (int i = startRow + 1; i < totalRows; i++) {
Element re = rootElement.getElement(i)
rowText = inputArea.document.getText(re.getStartOffset(), re.getEndOffset() - re.getStartOffset())
if (!rowText?.trim()) {
break
}
endBlockPos = re.getEndOffset()
}
inputArea.setSelectionStart(startBlockPos)
inputArea.setSelectionEnd(endBlockPos)
}
void showMessage(String message) {
statusLabel.text = message
}
void showExecutingMessage() {
statusLabel.text = 'Script executing now. Please wait or use "Interrupt Script" option.'
}
void showCompilingMessage() {
statusLabel.text = 'Script compiling now. Please wait.'
}
// Shows the detached 'outputArea' dialog
void showOutputWindow(EventObject evt = null) {
if (detachedOutput) {
outputWindow.setLocationRelativeTo(frame)
outputWindow.show()
}
}
void hideOutputWindow(EventObject evt = null) {
if (detachedOutput) {
outputWindow.visible = false
}
}
void hideAndClearOutputWindow(EventObject evt = null) {
clearOutput()
hideOutputWindow()
}
void smallerFont(EventObject evt = null) {
updateFontSize(inputArea.font.size - 2)
}
void smartHighlighter(EventObject evt = null) {
inputEditor.enableHighLighter(evt.source.selected ? SmartDocumentFilter : GroovyFilter)
inputEditor.textEditor.setText(inputEditor.textEditor.getText()) // enable the highlighter immediately
prefs.putBoolean('smartHighlighter', evt.source.selected)
}
void updateTitle() {
if (frame.title) {
String title = 'GroovyConsole'
if (indy) {
title += ' (Indy)'
}
if (scriptFile != null) {
frame.title = scriptFile.name + (dirty ? ' * ' : '') + ' - ' + title
} else {
frame.title = title
}
}
}
private updateFontSize(newFontSize) {
if (newFontSize > 40) {
newFontSize = 40
} else if (newFontSize < 4) {
newFontSize = 4
}
prefs.putInt('fontSize', newFontSize)
// don't worry, the fonts won't be changed to this family, the styles will only derive from this
def newFont = new Font(inputEditor.defaultFamily, Font.PLAIN, newFontSize)
inputArea.font = newFont
outputArea.font = newFont
}
void invokeTextAction(evt, closure, area = inputArea) {
def source = evt.getSource()
if (source != null) {
closure(area)
}
}
void cut(EventObject evt = null) {
invokeTextAction(evt, { source -> source.cut() })
}
void copy(EventObject evt = null) {
invokeTextAction(evt, { source -> source.copy() }, copyFromComponent ?: inputArea)
}
void paste(EventObject evt = null) {
invokeTextAction(evt, { source -> source.paste() })
}
void selectAll(EventObject evt = null) {
invokeTextAction(evt, { source -> source.selectAll() }, copyFromComponent ?: inputArea)
}
void setRowNumAndColNum() {
cursorPos = inputArea.getCaretPosition()
rowNum = rootElement.getElementIndex(cursorPos) + 1
def rowElement = rootElement.getElement(rowNum - 1)
colNum = cursorPos - rowElement.getStartOffset() + 1
rowNumAndColNum.setText("$rowNum:$colNum")
}
void print(EventObject evt = null) {
inputEditor.printAction.actionPerformed(evt)
}
void undo(EventObject evt = null) {
inputEditor.undoAction.actionPerformed(evt)
}
void redo(EventObject evt = null) {
inputEditor.redoAction.actionPerformed(evt)
}
void hyperlinkUpdate(HyperlinkEvent e) {
if (e.eventType == HyperlinkEvent.EventType.ACTIVATED) {
// URL of the form: file://myscript.groovy:32
String url = e.getURL()
int lineNumber = url[(url.lastIndexOf(':') + 1)..-1].toInteger()
def editor = inputEditor.textEditor
def text = editor.text
int newlineBefore = 0
int newlineAfter = 0
int currentLineNumber = 1
// let's find the previous and next newline surrounding the offending line
int i = 0
for (ch in text) {
if (ch == '\n') {
currentLineNumber++
}
if (currentLineNumber == lineNumber) {
newlineBefore = i
def nextNewline = text.indexOf('\n', i + 1)
newlineAfter = nextNewline > -1 ? nextNewline : text.length()
break
}
i++
}
// highlight / select the whole line
editor.setCaretPosition(newlineBefore)
editor.moveCaretPosition(newlineAfter)
}
}
void componentHidden(ComponentEvent e) {}
void componentMoved(ComponentEvent e) {}
void componentResized(ComponentEvent e) {
def component = e.getComponent()
if (component == outputArea || component == inputArea) {
def rect = component.getVisibleRect()
prefs.putInt("${component.name}Width", rect.getWidth().intValue())
prefs.putInt("${component.name}Height", rect.getHeight().intValue())
} else {
prefs.putInt("${component.name}Width", component.width)
prefs.putInt("${component.name}Height", component.height)
}
}
void componentShown(ComponentEvent e) {}
void focusGained(FocusEvent e) {
// remember component with focus for text-copy functionality
if (e.component == outputArea || e.component == inputArea) {
copyFromComponent = e.component
}
}
void focusLost(FocusEvent e) {}
private static boolean isWindows() {
return getOsName().startsWith("windows")
}
private static String getOsName() {
return System.getProperty("os.name").toLowerCase()
}
}
@CompileStatic
class GroovyFileFilter extends FileFilter {
private static final List GROOVY_SOURCE_EXTENSIONS = ['*.groovy', '*.gvy', '*.gy', '*.gsh', '*.story', '*.gpp', '*.grunit']
private static final GROOVY_SOURCE_EXT_DESC = GROOVY_SOURCE_EXTENSIONS.join(',')
boolean accept(File f) {
if (f.isDirectory()) {
return true
}
GROOVY_SOURCE_EXTENSIONS.find { it == getExtension(f) } ? true : false
}
String getDescription() {
"Groovy Source Files ($GROOVY_SOURCE_EXT_DESC)"
}
static String getExtension(File f) {
def ext = null
def s = f.getName()
def i = s.lastIndexOf('.')
if (i > 0 && i < s.length() - 1) {
ext = s.substring(i).toLowerCase()
}
"*$ext"
}
}