package org.radrails.rails.internal.ui.console; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import org.eclipse.core.commands.AbstractHandler; import org.eclipse.core.commands.ExecutionEvent; import org.eclipse.core.commands.ExecutionException; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IResource; import org.eclipse.core.resources.IResourceChangeEvent; import org.eclipse.core.resources.IResourceChangeListener; import org.eclipse.core.resources.IResourceDelta; import org.eclipse.core.resources.IWorkspaceRoot; import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IConfigurationElement; import org.eclipse.core.runtime.IExtensionPoint; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Platform; import org.eclipse.core.runtime.Status; import org.eclipse.core.runtime.jobs.ISchedulingRule; import org.eclipse.core.runtime.jobs.Job; import org.eclipse.debug.core.DebugEvent; import org.eclipse.debug.core.DebugException; import org.eclipse.debug.core.DebugPlugin; import org.eclipse.debug.core.IDebugEventSetListener; import org.eclipse.debug.core.ILaunch; import org.eclipse.debug.core.IStreamListener; import org.eclipse.debug.core.model.IFlushableStreamMonitor; import org.eclipse.debug.core.model.IProcess; import org.eclipse.debug.core.model.IStreamMonitor; import org.eclipse.debug.core.model.IStreamsProxy; import org.eclipse.debug.internal.ui.DebugUIPlugin; import org.eclipse.debug.internal.ui.preferences.IDebugPreferenceConstants; import org.eclipse.debug.internal.ui.viewers.AsynchronousSchedulingRuleFactory; import org.eclipse.debug.ui.IDebugUIConstants; import org.eclipse.debug.ui.console.IConsoleColorProvider; import org.eclipse.debug.ui.console.IConsoleHyperlink; import org.eclipse.debug.ui.console.IConsoleLineTracker; import org.eclipse.jface.preference.IPreferenceStore; import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.DocumentEvent; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.IDocumentListener; import org.eclipse.jface.text.IRegion; import org.eclipse.jface.text.contentassist.ContentAssistEvent; import org.eclipse.jface.text.contentassist.ContentAssistant; import org.eclipse.jface.text.contentassist.ICompletionListener; import org.eclipse.jface.text.contentassist.ICompletionProposal; import org.eclipse.swt.SWT; import org.eclipse.swt.custom.VerifyKeyListener; import org.eclipse.swt.events.VerifyEvent; import org.eclipse.swt.widgets.Display; import org.eclipse.ui.IPartListener; import org.eclipse.ui.IWorkbenchPart; import org.eclipse.ui.console.ConsolePlugin; import org.eclipse.ui.console.IConsole; import org.eclipse.ui.console.IConsoleView; import org.eclipse.ui.console.IHyperlink; import org.eclipse.ui.console.IOConsole; import org.eclipse.ui.console.IOConsoleInputStream; import org.eclipse.ui.console.IOConsoleOutputStream; import org.eclipse.ui.handlers.IHandlerService; import org.eclipse.ui.internal.console.IOConsolePage; import org.eclipse.ui.internal.console.IOConsoleViewer; import org.eclipse.ui.part.IPageBookViewPage; import org.eclipse.ui.progress.UIJob; import org.radrails.rails.core.RailsLog; import org.radrails.rails.internal.core.RailsPlugin; import org.radrails.rails.ui.RailsUILog; import org.radrails.rails.ui.RailsUIPlugin; import org.radrails.rails.ui.console.RailsShellCommandProvider; import org.rubypeople.rdt.launching.IRubyLaunchConfigurationConstants; import org.rubypeople.rdt.launching.ITerminal; import com.aptana.ide.core.IdeLog; import com.aptana.rdt.rake.IRakeHelper; import com.aptana.rdt.rake.RakePlugin; public class RailsShell extends IOConsole implements IPartListener, IDocumentListener, ITerminal, IResourceChangeListener, ICompletionListener { private static final String ENCODING = "UTF-8"; private static final String OUTPUT_PARTITION_TYPE = "org.eclipse.ui.console.io_console_output_partition_type"; private static final String INPUT_PARTITION_TYPE = "org.eclipse.ui.console.io_console_input_partition_type"; private static List<RailsShellCommandProvider> fgProviders; private IOConsolePage page; private IOConsoleOutputStream fOutputStream; private IOConsoleInputStream fInput; private StreamListener listener; private IConsoleColorProvider fColorProvider; private IProcess fProcess; private ConsoleLineNotifier consoleLineNotifier; private IProject fProject; private ContentAssistant fContentAssistant; private RailsShellContentAssistProcessor fContentAssistProcessor; private IStreamsProxy streamsProxy; private IOConsoleOutputStream fFakeInputStream; private IOConsoleOutputStream fErrorStream; private Map<String, StreamListener> listeners; private boolean fActivated; private RailsShellExecutor executor; /** * For jobs that are spitting out output, force them to run serially */ private ISchedulingRule rule = AsynchronousSchedulingRuleFactory.getDefault().newSerialRule(); private ArrayList<String> fCommandHistory; private int fCommandIndex; private InputReadJob readJob; private boolean completionActive; protected ILaunch currentLaunch; public RailsShell() { super("Rails Shell", IRailsShellConstants.TERMINAL_ID, RailsUIPlugin.getImageDescriptor("icons/rails.gif"), ENCODING, true); fInput = getInputStream(); fColorProvider = DebugUIPlugin.getDefault().getProcessConsoleManager().getColorProvider( IRubyLaunchConfigurationConstants.ID_RUBY_PROCESS_TYPE); fInput.setColor(fColorProvider.getColor(IDebugUIConstants.ID_STANDARD_INPUT_STREAM)); fContentAssistProcessor = new RailsShellContentAssistProcessor(); listeners = new HashMap<String, StreamListener>(); executor = new RailsShellExecutor(this); Set<IProject> projects = RailsPlugin.getRailsProjects(); if (projects != null && !projects.isEmpty()) setProject(projects.iterator().next()); ResourcesPlugin.getWorkspace().addResourceChangeListener(this, IResourceChangeEvent.POST_CHANGE); fCommandHistory = new ArrayList<String>(); } @Override public IPageBookViewPage createPage(IConsoleView view) { page = (IOConsolePage) super.createPage(view); IDocument doc = getDocument(); IHandlerService service = (IHandlerService) view.getSite().getService(IHandlerService.class); service.activateHandler("org.eclipse.ui.edit.text.contentAssist.proposals", new AbstractHandler() { public Object execute(ExecutionEvent evt) throws ExecutionException { fContentAssistant.showPossibleCompletions(); return null; } }); doc.addDocumentListener(this); view.getSite().getPage().addPartListener(this); return page; } public void attach(IProcess process) { if (fProcess != null) { // dicsonnect old process removePatternMatchListener(consoleLineNotifier); } fProcess = process; IConsoleLineTracker[] lineTrackers = DebugUIPlugin.getDefault().getProcessConsoleManager().getLineTrackers( process); if (lineTrackers.length > 0) { consoleLineNotifier = new ConsoleLineNotifier(); addPatternMatchListener(consoleLineNotifier); } connect(process.getStreamsProxy()); } public void connect(IStreamsProxy streamsProxy) { IPreferenceStore store = DebugUIPlugin.getDefault().getPreferenceStore(); IStreamMonitor streamMonitor = streamsProxy.getErrorStreamMonitor(); if (streamMonitor != null) { connect(streamMonitor, IDebugUIConstants.ID_STANDARD_ERROR_STREAM); } streamMonitor = streamsProxy.getOutputStreamMonitor(); if (streamMonitor != null) { connect(streamMonitor, IDebugUIConstants.ID_STANDARD_OUTPUT_STREAM); } if (readJob == null) { readJob = new InputReadJob(streamsProxy); readJob.setSystem(true); readJob.schedule(); } else { readJob.setStreamProxy(streamsProxy); } this.streamsProxy = streamsProxy; } public void connect(IStreamMonitor streamMonitor, String streamIdentifier) { synchronized (streamMonitor) { StreamListener listener = listeners.get(streamIdentifier); if (listener != null) { listener.getStreamMonitor().removeListener(listener); } listener = new StreamListener(streamIdentifier, streamMonitor, getStream(streamIdentifier)); listeners.put(streamIdentifier, listener); } } @Override public void activate() { super.activate(); if (!fActivated) { fOutputStream = newOutputStream(); fOutputStream.setColor(fColorProvider.getColor(IDebugUIConstants.ID_STANDARD_OUTPUT_STREAM)); fErrorStream = newOutputStream(); fErrorStream.setColor(fColorProvider.getColor(IDebugUIConstants.ID_STANDARD_ERROR_STREAM)); fFakeInputStream = newOutputStream(); fFakeInputStream.setColor(fColorProvider.getColor(IDebugUIConstants.ID_STANDARD_INPUT_STREAM)); // Enable activateOnWrite only after first prompt IOConsoleOutputStream standardOutputStream = getStream(IDebugUIConstants.ID_STANDARD_OUTPUT_STREAM); if (standardOutputStream != null) { standardOutputStream.setActivateOnWrite(false); } IOConsoleOutputStream standardErrorStream = getStream(IDebugUIConstants.ID_STANDARD_ERROR_STREAM); if (standardOutputStream != null) { standardOutputStream.setActivateOnWrite(false); } try { write( IDebugUIConstants.ID_STANDARD_INPUT_STREAM, "Welcome to the Rails Shell. This view is meant for advanced users and command line lovers as a text-based way\nto run rails " + "commands such as: rails, script/generate, script/plugin, gem, rake, etc.\nThis shell can replace the functionality of the " + "Rake Tasks, Rails Plugins, and generators views.\n\n" + IRailsShellConstants.PROMPT); } finally { // Enable activateOnWrite only after first prompt IPreferenceStore iPreferenceStore = DebugUIPlugin.getDefault().getPreferenceStore(); if (standardOutputStream != null) { standardOutputStream.setActivateOnWrite(iPreferenceStore.getBoolean(IDebugPreferenceConstants.CONSOLE_OPEN_ON_OUT)); } if (standardOutputStream != null) { standardOutputStream.setActivateOnWrite(iPreferenceStore.getBoolean(IDebugPreferenceConstants.CONSOLE_OPEN_ON_OUT)); } } DebugPlugin.getDefault().addDebugEventListener(new IDebugEventSetListener() { public void handleDebugEvents(DebugEvent[] events) { if (events == null) return; for (int i = 0; i < events.length; i++) { if (events[i].getKind() != DebugEvent.TERMINATE) continue; if (!(events[i].getSource() instanceof IProcess)) continue; IProcess source = (IProcess) events[i].getSource(); ILaunch launch = source.getLaunch(); String id = launch.getAttribute(IRubyLaunchConfigurationConstants.ATTR_USE_TERMINAL); if (id != null && id.equals(IRailsShellConstants.TERMINAL_ID)) { writePrompt(); fProcess = null; } } } }); UIJob job = new UIJob("") { private int tries = 3; public IStatus runInUIThread(IProgressMonitor monitor) { if (page == null || page.getViewer() == null || page.getViewer().getTextWidget() == null) { if (tries-- > 0) schedule(2000); return Status.CANCEL_STATUS; } // Override HOME/Tab/Ctrl+C key combos page.getViewer().getTextWidget().addVerifyKeyListener(new VerifyKeyListener() { public void verifyKey(VerifyEvent e) { int keyCode = e.keyCode; if (keyCode == 'c' && e.stateMask == SWT.CONTROL) // 'Ctrl+c' { // Kill the current launch! if (fProcess != null && fProcess.canTerminate()) { try { fProcess.terminate(); } catch (DebugException e1) { IdeLog.logError(RailsUIPlugin.getInstance(), e1.getMessage(), e1); } } return; } if (keyCode == 9) { // Tab e.doit = false; fContentAssistant.showPossibleCompletions(); return; } if (keyCode == 16777223) { // HOME e.doit = false; String contents = page.getViewer().getDocument().get(); int offset = contents.lastIndexOf(IRailsShellConstants.PROMPT); if (offset == -1) { offset = page.getViewer().getTextWidget().getCharCount(); } else { offset++; } page.getViewer().getTextWidget().setCaretOffset(offset); } else { if (completionActive) { return; } String command = null; if (isUpArrow(e)) { e.doit = false; // FIXME For whatever reason this isn't stopping cursor from moving command = getCommandFromHistory(-1); } else if (isDownArrow(e)) { e.doit = false; command = getCommandFromHistory(1); } else { return; } if (command == null) return; try { setCommand(command); page.getViewer().getTextWidget().setCaretOffset(getDocument().getLength()); } catch (Exception e1) { RailsLog.log(e1); } } } private void setCommand(String command) throws BadLocationException { // Back up to beginning of first line and replace from there to end of document int lines = getDocument().getNumberOfLines(); int offset = getDocument().getLineOffset(lines - 1); int length = getDocument().getLength() - offset; String toReplace = getDocument().get(offset, length); if (toReplace.trim().startsWith(IRailsShellConstants.PROMPT)) { int index = toReplace.indexOf(IRailsShellConstants.PROMPT); offset += index + IRailsShellConstants.PROMPT.length(); length -= index + IRailsShellConstants.PROMPT.length(); } getDocument().replace(offset, length, command); } private boolean isUpArrow(VerifyEvent e) { return e.keyCode == 16777217; } private boolean isDownArrow(VerifyEvent e) { return e.keyCode == 16777218; } }); return Status.OK_STATUS; } }; job.schedule(); fActivated = true; } } protected String getCommandFromHistory(int move) { if (fCommandHistory.isEmpty()) return null; fCommandIndex += move; if (fCommandIndex <= 0) { fCommandIndex = 0; } else if (fCommandIndex >= fCommandHistory.size()) { fCommandIndex = fCommandHistory.size(); return ""; } return fCommandHistory.get(fCommandIndex); } private void writePrompt() { write(IDebugUIConstants.ID_STANDARD_INPUT_STREAM, IRailsShellConstants.PROMPT); } private void installContentAssist() { if (page == null || fContentAssistant != null) return; IOConsoleViewer viewer = (IOConsoleViewer) page.getViewer(); if (viewer == null) return; fContentAssistant = new ContentAssistant(); fContentAssistant.setContentAssistProcessor(fContentAssistProcessor, INPUT_PARTITION_TYPE); fContentAssistant.setContentAssistProcessor(fContentAssistProcessor, OUTPUT_PARTITION_TYPE); fContentAssistant.enableAutoActivation(true); fContentAssistant.enableAutoInsert(true); fContentAssistant.enablePrefixCompletion(true); fContentAssistant.install(viewer); fContentAssistant.addCompletionListener(this); } public void partActivated(IWorkbenchPart part) { if (part instanceof IConsoleView) { page.setFocus(); } } public void partBroughtToTop(IWorkbenchPart part) { installContentAssist(); } public void partClosed(IWorkbenchPart part) { } public void partDeactivated(IWorkbenchPart part) { } public void partOpened(IWorkbenchPart part) { if (part instanceof IConsoleView) { installContentAssist(); } } public void documentAboutToBeChanged(DocumentEvent event) { installContentAssist(); } public void documentChanged(DocumentEvent event) { installContentAssist(); String text = event.getText(); IDocument doc = event.getDocument(); String[] delimeters = doc.getLegalLineDelimiters(); if (contains(text, delimeters)) { // We hit a newline, execute the current command! String contents = doc.get(); int index = contents.lastIndexOf("\n", event.getOffset() - text.length()); if (index == -1) index = contents.lastIndexOf("\r", event.getOffset() - text.length()); // Handle \r too if (index != -1) contents = contents.substring(index + 1); // If it doesn't contain the prompt, it's probably not a command! index = contents.indexOf(IRailsShellConstants.PROMPT); if (index == -1) return; // strip the prompt contents = contents.substring(index + 1).trim(); fCommandHistory.add(contents); fCommandIndex = fCommandHistory.size(); executor.run(fProject, contents); } } protected IRakeHelper getRakeTasksHelper() { return RakePlugin.getDefault().getRakeHelper(); } private boolean contains(String text, String[] delimeters) { for (int i = 0; i < delimeters.length; i++) { if (text.equals(delimeters[i])) return true; } return false; } private class StreamListener implements IStreamListener { private IOConsoleOutputStream fStream; private IStreamMonitor fStreamMonitor; private String fStreamId; private boolean fFlushed = false; private boolean fListenerRemoved = false; public StreamListener(String streamIdentifier, IStreamMonitor monitor, IOConsoleOutputStream stream) { this.fStreamId = streamIdentifier; this.fStreamMonitor = monitor; this.fStream = stream; fStreamMonitor.addListener(this); // fix to bug 121454. Ensure that output to fast processes is processed. streamAppended(null, monitor); } /* * (non-Javadoc) * @see org.eclipse.debug.core.IStreamListener#streamAppended(java.lang.String, * org.eclipse.debug.core.model.IStreamMonitor) */ public void streamAppended(String text, IStreamMonitor monitor) { if (fFlushed) { try { if (fStream != null) { fStream.write(text); } } catch (IOException e) { DebugUIPlugin.log(e); } } else { String contents = null; synchronized (fStreamMonitor) { fFlushed = true; contents = fStreamMonitor.getContents(); if (fStreamMonitor instanceof IFlushableStreamMonitor) { IFlushableStreamMonitor m = (IFlushableStreamMonitor) fStreamMonitor; m.flushContents(); m.setBuffered(false); } } try { if (contents != null && contents.length() > 0) { if (fStream != null) { fStream.write(contents); } } } catch (IOException e) { DebugUIPlugin.log(e); } } } public IStreamMonitor getStreamMonitor() { return fStreamMonitor; } public void closeStream() { if (fStreamMonitor == null) { return; } synchronized (fStreamMonitor) { fStreamMonitor.removeListener(this); if (!fFlushed) { String contents = fStreamMonitor.getContents(); streamAppended(contents, fStreamMonitor); } fListenerRemoved = true; try { if (fStream != null) { fStream.close(); } } catch (IOException e) { } } } public void dispose() { if (!fListenerRemoved) { closeStream(); } fStream = null; fStreamMonitor = null; fStreamId = null; } } public void addLink(IConsoleHyperlink link, int offset, int length) { try { addHyperlink(link, offset, length); } catch (BadLocationException e) { DebugUIPlugin.log(e); } } public void addLink(IHyperlink link, int offset, int length) { try { addHyperlink(link, offset, length); } catch (BadLocationException e) { DebugUIPlugin.log(e); } } public IProcess getProcess() { return fProcess; } public IRegion getRegion(IConsoleHyperlink link) { return super.getRegion(link); } public IOConsoleOutputStream getStream(String streamIdentifier) { if (streamIdentifier.equals(IDebugUIConstants.ID_STANDARD_ERROR_STREAM)) return fErrorStream; if (streamIdentifier.equals(IDebugUIConstants.ID_STANDARD_INPUT_STREAM)) return fFakeInputStream; return fOutputStream; } public void setProject(final IProject project) { this.fProject = project; fContentAssistProcessor.setProject(project); Display.getDefault().asyncExec(new Runnable() { public void run() { if (project == null) setName("Rails Shell (" + RailsPlugin.getInstance().getRailsPath() + ")"); else setName("Rails Shell - " + project.getName() + " (" + RailsPlugin.getInstance().getRailsPath() + ")"); } }); } public void write(final String streamIdentifier, final String text) { if (streamIdentifier.equals(IDebugUIConstants.ID_STANDARD_INPUT_STREAM) && text.endsWith("\n")) { fCommandHistory.add(text.substring(0, text.length() - 1)); } UIJob job = new UIJob("") { @Override public IStatus runInUIThread(IProgressMonitor monitor) { try { IOConsoleOutputStream stream = getStream(streamIdentifier); stream.write(text); return Status.OK_STATUS; } catch (IOException e) { RailsUILog.log(e); return new Status(Status.ERROR, RailsUIPlugin.getPluginIdentifier(), -1, "Error writing text to rails shell", e); } } }; job.setPriority(Job.INTERACTIVE); job.setRule(rule); job.schedule(5); } @Override public void clearConsole() { super.clearConsole(); writePrompt(); } public void resourceChanged(IResourceChangeEvent event) { if (event == null) return; IResourceDelta delta = event.getDelta(); traverseDelta(delta); } /** * boolean indicates if we should continue to traverse. * * @param delta * @return */ private boolean traverseDelta(IResourceDelta delta) { if (delta == null) return true; IResource res = delta.getResource(); if (res instanceof IWorkspaceRoot) { IResourceDelta[] children = delta.getAffectedChildren(); for (int i = 0; i < children.length; i++) { if (!traverseDelta(children[i])) return false; } return true; } if (!(res instanceof IProject)) return false; int deltaKind = delta.getKind(); if (deltaKind != IResourceDelta.ADDED && deltaKind != IResourceDelta.REMOVED) return false; IProject project = (IProject) res; if (deltaKind == IResourceDelta.ADDED) { if (RailsPlugin.hasRailsNature(project)) { setProject(project); } return false; } if (fProject != null && !project.equals(fProject)) return false; Set<IProject> projects = RailsPlugin.getRailsProjects(); for (final IProject possible : projects) { if (!possible.equals(project)) { Display.getDefault().asyncExec(new Runnable() { public void run() { setProject(possible); } }); return false; } } setProject(null); return false; } public static RailsShell open() { RailsShell console = new RailsShell(); ConsolePlugin conMan = ConsolePlugin.getDefault(); conMan.getConsoleManager().addConsoles(new IConsole[] { console }); console.activate(); return console; } private class InputReadJob extends Job { private IStreamsProxy streamsProxy; InputReadJob(IStreamsProxy streamsProxy) { super("Process Console Input Job"); //$NON-NLS-1$ this.streamsProxy = streamsProxy; } public void setStreamProxy(IStreamsProxy streamsProxy2) { this.streamsProxy = streamsProxy2; } /* * (non-Javadoc) * @see org.eclipse.core.runtime.jobs.Job#run(org.eclipse.core.runtime.IProgressMonitor) */ protected IStatus run(IProgressMonitor monitor) { try { byte[] b = new byte[1024]; int read = 0; while (fInput != null && read >= 0) { read = fInput.read(b); if (read > 0) { String s = new String(b, 0, read); // FIXME If there' no active launch associated with this shell, don't pipe it, it's a new // command at the prompt if (fProcess != null && !fProcess.isTerminated()) { streamsProxy.write(s); } } } } catch (IOException e) { RailsUILog.log(e); } return Status.OK_STATUS; } } public void assistSessionEnded(ContentAssistEvent event) { completionActive = false; } public void assistSessionStarted(ContentAssistEvent event) { completionActive = true; } public void selectionChanged(ICompletionProposal proposal, boolean smartToggle) { } public static List<RailsShellCommandProvider> getCommandProviders(IProject fProject, String fRunMode) { List<RailsShellCommandProvider> providers = getCommandProviders(); for (RailsShellCommandProvider provider : providers) { provider.initialize(fProject, fRunMode); } return providers; } private static List<RailsShellCommandProvider> getCommandProviders() { if (fgProviders == null) { final Map<RailsShellCommandProvider, Integer> providersToPriority = new HashMap<RailsShellCommandProvider, Integer>(); IExtensionPoint extension = Platform.getExtensionRegistry().getExtensionPoint( RailsUIPlugin.getPluginIdentifier(), "railsShellCommandProviders"); if (extension == null) return Collections.emptyList(); // for all extensions of this point... IConfigurationElement[] configElements = extension.getConfigurationElements(); for (int j = 0; j < configElements.length; j++) { try { RailsShellCommandProvider provider = (RailsShellCommandProvider) configElements[j] .createExecutableExtension("class"); String rawPriority = configElements[j].getAttribute("priority"); int priority = 50; try { priority = Integer.parseInt(rawPriority); } catch (Exception e) { // ignore } providersToPriority.put(provider, priority); } catch (CoreException e) { RailsUILog.log(e); } } // Sort providers by ascending priority List<RailsShellCommandProvider> providers = new ArrayList<RailsShellCommandProvider>(providersToPriority .keySet()); Collections.sort(providers, new Comparator<RailsShellCommandProvider>() { public int compare(RailsShellCommandProvider o1, RailsShellCommandProvider o2) { return providersToPriority.get(o1).compareTo(providersToPriority.get(o2)); } }); fgProviders = providers; } return fgProviders; } }