blob: a37dc76f61dade361809590c9ef11d1eb0ee38fd [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.ui
import groovy.inspect.swingui.AstBrowser
import groovy.inspect.swingui.ObjectBrowser
import groovy.swing.SwingBuilder
import groovy.transform.ThreadInterrupt
import groovy.ui.text.FindReplaceUtility
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.syntax.SyntaxException
import javax.swing.*
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.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.*
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.List
import java.util.prefs.Preferences
/**
* Groovy Swing console.
*
* Allows user to interactively enter and execute Groovy.
*
* @author Danno Ferrin
* @author Dierk Koenig, changed Layout, included Selection sensitivity, included ObjectBrowser
* @author Alan Green more features: history, System.out capture, bind result to _
* @author Guillaume Laforge, stacktrace hyperlinking to the current script line
* @author Hamlet D'Arcy, AST browser
* @author Roshan Dawrani
* @author Paul King
* @author Andre Steingress
*/
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 = []
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
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
boolean saveOnRun = prefs.getBoolean('saveOnRun', false)
Action saveOnRunAction
//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
// 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 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/ui/ConsoleIcon.png') // used by ObjectBrowser and AST Viewer
public static URL NODE_ICON_PATH = Console.class.classLoader.getResource('groovy/ui/icons/bullet_green.png') // used by AST Viewer
static groovyFileFilter = new GroovyFileFilter()
boolean scriptRunning = false
boolean stackOverFlowError = false
Action interruptAction
static void main(args) {
if (args.length == 1 && args[0] == '--help') {
println '''usage: groovyConsole [options] [filename]
options:
--help This Help message
-cp,-classpath,--classpath <path> Specify classpath'''
return
}
// full stack trace should not be logged to the output window - GROOVY-4663
java.util.logging.Logger.getLogger(StackTraceUtils.STACK_LOG_NAME).useParentHandlers = false
//when starting via main set the look and feel to system
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName())
def console = new Console(Console.class.classLoader?.getRootLoader())
console.useScriptClassLoaderForScriptExecution = true
console.run()
if (args.length == 1) console.loadScriptFile(args[0] as File)
}
Console() {
this(new Binding())
}
Console(Binding binding) {
this(null, binding)
}
Console(ClassLoader parent) {
this(parent, new Binding())
}
Console(ClassLoader parent, Binding binding) {
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.ui.ConsoleIvyPlugin')
ivyPluginClass.newInstance().addListener(this)
}
} catch(ClassNotFoundException ignore) { }
binding.variables._outputTransforms = OutputTransforms.loadOutputTransforms()
}
void newScript(ClassLoader parent, Binding binding) {
config = new CompilerConfiguration()
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/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)
}
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.
groovy.lang.
gjdk.groovy.lang.
sun.
java.lang.reflect.
java.lang.Thread
groovy.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 java.awt.Window) {
nativeFullScreenForMac(swing.consoleFrame)
swing.consoleFrame.pack()
swing.consoleFrame.show()
}
installInterceptor()
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(java.awt.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
}
''')
}
}
public 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
doc.insertString(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);
doc.insertString(initialLength, line[0..<index], stacktraceStyle)
doc.insertString(initialLength + index, line[index..<(index + length)], style)
doc.insertString(initialLength + index + length, line[(index + length)..-1] + '\n', stacktraceStyle)
} else {
doc.insertString(initialLength, line + '\n', stacktraceStyle)
}
}
ensureNoDocLengthOverflow(doc)
}
// 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')
doc.insertString(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
doc.insertString(len, ' \n', style)
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 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
customizers.clear()
if (threadInterrupt) {
config.addCompilationCustomizers(new ASTTransformationCustomizer(ThreadInterrupt))
}
}
void caretUpdate(CaretEvent e){
textSelectionStart = Math.min(e.dot,e.mark)
textSelectionEnd = Math.max(e.dot,e.mark)
setRowNumAndColNum()
}
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 exit(EventObject evt = null) {
if(askToInterruptScript()) {
if (askToSaveFile()) {
if (frame instanceof java.awt.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);
doc.insertString(doc.length, message + ' at ', stacktraceStyle)
doc.insertString(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
doc.insertString(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)
StringWriter sw = new StringWriter()
new PrintWriter(sw).withWriter {pw -> StackTraceUtils.deepSanitize(t).printStackTrace(pw) }
appendStacktrace("\n${sw.buffer}\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()).run({ inputArea.getText() } )
}
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) {
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 runSelectedScript(EventObject evt = null) {
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 clearContext(EventObject evt = null) {
def binding = new Binding()
newScript(null, binding)
// reload output transforms
binding.variables._outputTransforms = OutputTransforms.loadOutputTransforms()
}
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.getProperty('line.separator')
def record = new HistoryRecord( allText: inputArea.getText().replaceAll(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()
}
}
}
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.getProperty('line.separator')
def record = new HistoryRecord( allText: inputArea.getText().replaceAll(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.getProperty('line.separator')
if (historyIndex >= history.size()) {
pendingRecord = new HistoryRecord( allText: inputArea.getText().replaceAll(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 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 updateTitle() {
if (frame.properties.containsKey('title')) {
if (scriptFile != null) {
frame.title = scriptFile.name + (dirty?' * ':'') + ' - GroovyConsole'
} else {
frame.title = 'GroovyConsole'
}
}
}
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)
}
}
public void componentShown(ComponentEvent e) { }
public void focusGained(FocusEvent e) {
// remember component with focus for text-copy functionality
if (e.component == outputArea || e.component == inputArea) {
copyFromComponent = e.component
}
}
public void focusLost(FocusEvent e) { }
}
class GroovyFileFilter extends FileFilter {
private static final GROOVY_SOURCE_EXTENSIONS = ['*.groovy', '*.gvy', '*.gy', '*.gsh', '*.story', '*.gpp', '*.grunit']
private static final GROOVY_SOURCE_EXT_DESC = GROOVY_SOURCE_EXTENSIONS.join(',')
public boolean accept(File f) {
if (f.isDirectory()) {
return true
}
GROOVY_SOURCE_EXTENSIONS.find {it == getExtension(f)} ? true : false
}
public String getDescription() {
"Groovy Source Files ($GROOVY_SOURCE_EXT_DESC)"
}
static String getExtension(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"
}
}