Package ccw.repl

Source Code of ccw.repl.REPLView$NamespaceRefreshFocusListener

package ccw.repl;

import java.io.IOException;
import java.net.MalformedURLException;
import java.net.SocketException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicReference;
import java.util.regex.Pattern;

import org.eclipse.core.commands.AbstractHandler;
import org.eclipse.core.commands.ExecutionEvent;
import org.eclipse.core.commands.ExecutionException;
import org.eclipse.core.runtime.IAdaptable;
import org.eclipse.core.runtime.Platform;
import org.eclipse.debug.core.DebugException;
import org.eclipse.debug.core.ILaunch;
import org.eclipse.jface.action.Action;
import org.eclipse.jface.action.IStatusLineManager;
import org.eclipse.jface.action.IToolBarManager;
import org.eclipse.jface.dialogs.ErrorDialog;
import org.eclipse.jface.dialogs.InputDialog;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.jface.resource.JFaceResources;
import org.eclipse.jface.text.Document;
import org.eclipse.jface.text.source.ISourceViewer;
import org.eclipse.jface.util.IPropertyChangeListener;
import org.eclipse.jface.util.PropertyChangeEvent;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.ST;
import org.eclipse.swt.custom.SashForm;
import org.eclipse.swt.custom.StyleRange;
import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.custom.VerifyKeyListener;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.events.FocusEvent;
import org.eclipse.swt.events.FocusListener;
import org.eclipse.swt.events.VerifyEvent;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.ui.IViewSite;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.PartInitException;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.console.ConsolePlugin;
import org.eclipse.ui.console.IConsole;
import org.eclipse.ui.contexts.IContextActivation;
import org.eclipse.ui.contexts.IContextService;
import org.eclipse.ui.handlers.IHandlerService;
import org.eclipse.ui.internal.editors.text.EditorsPlugin;
import org.eclipse.ui.part.ViewPart;
import org.eclipse.ui.texteditor.SourceViewerDecorationSupport;
import org.eclipse.ui.texteditor.StatusLineContributionItem;

import ccw.CCWPlugin;
import ccw.TraceOptions;
import ccw.editors.clojure.ClojureDocumentProvider;
import ccw.editors.clojure.ClojureSourceViewer;
import ccw.editors.clojure.ClojureSourceViewerConfiguration;
import ccw.editors.clojure.IClojureEditor;
import ccw.editors.clojure.IClojureEditorActionDefinitionIds;
import ccw.preferences.PreferenceConstants;
import ccw.util.ClojureInvoker;
import ccw.util.DisplayUtil;
import ccw.util.StringUtils;
import ccw.util.StyledTextUtil;
import ccw.util.TextViewerSupport;
import ccw.util.osgi.ClojureOSGi;
import clojure.lang.ExceptionInfo;
import clojure.lang.IFn;
import clojure.lang.ISeq;
import clojure.lang.Keyword;
import clojure.lang.PersistentHashMap;
import clojure.tools.nrepl.Connection;
import clojure.tools.nrepl.Connection.Response;

public class REPLView extends ViewPart implements IAdaptable, SafeConnection.IConnectionLostListener {

  private static final double DISCONNECTED_REPL_FG_TRANSPARENCY_PCT = 0.5;

  private final ClojureInvoker viewHelpers = ClojureInvoker.newInvoker(CCWPlugin.getDefault(), "ccw.repl.view-helpers");

  private final ClojureInvoker editorSupport = ClojureInvoker.newInvoker(
            CCWPlugin.getDefault(),
            "ccw.editors.clojure.editor-support");

  private final ClojureInvoker str = ClojureInvoker.newInvoker(
            CCWPlugin.getDefault(),
            "clojure.string");

  /* Keep this in sync with the context defined in plugin.xml */
  public static final String CCW_UI_CONTEXT_REPL = "ccw.ui.context.repl";

    public static final String VIEW_ID = "ccw.view.repl";
    public static final AtomicReference<REPLView> activeREPL = new AtomicReference<REPLView>();

    /**
     * This misery around secondary IDs is due to the fact that:
     * eclipse really, really wants views to be pre-defined; while you can have a view type that
     * has multiple instances, doing so requires having *stable* secondary IDs in order for the
     * IDE to retain positioning and size preferences.
     * We could just use UUIDs to identify different REPL views, but that means no position info
     * will be retained.  We could use nREPL URLs, but those are almost as bad as UUIDs insofar as
     * the autoselected ports are hardly ever used twice.
     * Try as I might, I cannot find a way to get a view to (a) be displayed in a particular place
     * in the IDE (short of defining a Clojure perspective!) or (b) be notified of when a REPL
     * view is moved to begin with.
     *
     * This approach will result in a user "training" the IDE where to put REPL views
     * (with seemingly intransigent or arbitrary behaviour to start); eventually, REPLs will
     * all end up displaying in the desired location.
     */
    private static Set<String> SECONDARY_VIEW_IDS = new TreeSet<String>() {{
       // no one will create more than 1000 REPLs at a time, right? :-P
       for (int i = 0; i < 1000; i++) add(String.format("%03d", i));
    }};
    private static String getSecondaryId () {
        synchronized (SECONDARY_VIEW_IDS) {
            String id = SECONDARY_VIEW_IDS.iterator().next();
            SECONDARY_VIEW_IDS.remove(id);
            return id;
        }
    }
    private static void releaseSecondaryId (String id) {
      assert id != null;

        synchronized (SECONDARY_VIEW_IDS) {
            SECONDARY_VIEW_IDS.add(id);
        }
    }

    private static final Keyword inputExprLogType = Keyword.intern("in-expr");
    private static final Keyword errLogType = Keyword.intern("err");
    private static final Keyword errorResponseKey = Keyword.intern("err");
    private static final Pattern boostIndent = Pattern.compile("^", Pattern.MULTILINE);

    // TODO would like to eliminate separate log view, but:
    // 1. PareditAutoEditStrategy gets all text from a source document, and bails badly if its not well-formed
    //      (and REPL output is guaranteed to not be well-formed)
    // 2. Even if (1) were fixed/changed, it's not clear to me how to "partition" an IDocument, or compose IDocuments
    //     so that we can have one range that is still *highlighted* for clojure content (and not editable),
    //     and another range that is editable and has full paredit, code completion, etc.
    public StyledText logPanel;
    /** record for colors used in logPanel */
    public final ClojureSourceViewer.EditorColors logPanelEditorColors = new ClojureSourceViewer.EditorColors();

    public ClojureSourceViewer viewer;

    public StyledText viewerWidget; // public only to simplify interop with helpers impl'd in Clojure

    private StyledTextPlaceHolder placeholder;

    private ClojureSourceViewerConfiguration viewerConfig;

    private final IPropertyChangeListener fontChangeListener = new IPropertyChangeListener() {
        @Override
    public void propertyChange(PropertyChangeEvent event) {
            if (event.getProperty().equals(JFaceResources.TEXT_FONT)) resetFont();
        }
    };

    private Connection interactive;
    private SafeConnection safeToolConnection;

    private IConsole console;
    private ILaunch launch;

    private String currentNamespace = "user";
    private Map<String, Object> describeInfo;
    private IFn evalExpression;
    private String sessionId;
    private String secondaryId;

    /* function implementing load previous/next command from history into input area */
    private IFn historyActionFn;

    public void setHistoryActionFn(IFn historyActionFn) {
      this.historyActionFn = historyActionFn;
    }
    public IFn getHistoryActionFn() {
      return this.historyActionFn;
    }

    private boolean isConnectionLost;

    private boolean inConnectionLost;
    @Override
    public void connectionLost() {
      if (inConnectionLost) {
        // Prevent infinite loops
        return;
      }

      inConnectionLost = true;
      try {
        if (launch != null && !launch.isTerminated()) {
          // Try to reconnect
          try {
          reconnect();
        } catch (Exception e) {
          e.printStackTrace();
          markAsLost();
        }
        } else {
          markAsLost();
        }
      } finally {
        inConnectionLost = false;
      }
    }

    public boolean isConnectionLost() {
      return isConnectionLost;
    }

    private void markAsLost() {
      isConnectionLost = true;
      try {
        closeConnections();
      } catch (Exception e) {
        e.printStackTrace();
      }

      // visual
      DisplayUtil.asyncExec(new Runnable() {
      @Override public void run() {
        setPartName("REPL disconnected");
          autoRepeatLastAction.setEnabled(false);
          printErrorAction.setEnabled(false);
          interruptAction.setEnabled(false);
          reconnectAction.setEnabled(false);
          clearLogAction.setEnabled(false);
          newSessionAction.setEnabled(false);
          showConsoleAction.setEnabled(false);
        activeREPL.compareAndSet(REPLView.this, null);
        if (viewerWidget != null && !viewerWidget.isDisposed()) {
          viewerWidget.setEditable(false);
          StyledTextUtil.lightenStyledTextColors(viewerWidget, 0.5);
        }
        if (logPanel != null && !logPanel.isDisposed()) {
          StyledTextUtil.lightenStyledTextColors(logPanel, DISCONNECTED_REPL_FG_TRANSPARENCY_PCT);
        }
      }
    });
    }

    public SafeConnection getSafeToolingConnection() {
      return safeToolConnection;
    }

    private SourceViewerDecorationSupport fSourceViewerDecorationSupport;
  private StatusLineContributionItem structuralEditionModeStatusContributionItem;

  /** Eclipse Preferences Listener */
  private IPropertyChangeListener prefsListener;

  /** Last expression sent via the REPL input area, as opposed to sent by CCW, or sent via an editor, etc. */
  private String lastExpressionSentFromREPL;

    public REPLView () {}

    @Override
    public void init(IViewSite site) throws PartInitException {
      super.init(site);
        activeREPL.set(REPLView.this);
    }

    private void resetFont () {
        Font font= JFaceResources.getTextFont();
        logPanel.setFont(font);
        viewerWidget.setFont(font);
    }

    private void copyToLog (StyledText s) {
        // sadly, need to reset text on the ST in order to get formatting/style ranges...
        s.setText(boostIndent.matcher(s.getText()).replaceAll("   ").replaceFirst("^\\s+", "=> "));
        int start = logPanel.getCharCount();
        try {
            viewHelpers._("log", this, logPanel, s.getText(), inputExprLogType);
            for (StyleRange sr : s.getStyleRanges()) {
                sr.start += start;
                logPanel.setStyleRange(sr);
            }
        } catch (Exception e) {
            // should never happen
            CCWPlugin.logError("Could not copy expression to log", e);
        }
    }

    private String removeTrailingSpaces(String s) {
      return (String) str._("trimr", s);
    }
    private void evalExpression () {
      // We remove trailing spaces so that we do not embark extra spaces,
      // newlines, etc. for example when evaluating after having hit the
      // Enter key (which automatically adds a new line
      viewerWidget.setText(removeTrailingSpaces(viewerWidget.getText()));
        evalExpression(viewerWidget.getText(), true, false, false);
        if (viewerWidget.getText().trim().length() > 0) {
          lastExpressionSentFromREPL = viewerWidget.getText();
        }
        copyToLog(viewerWidget);
        viewerWidget.setText("");
    }

    public String getLastExpressionSentFromREPL() {
      return lastExpressionSentFromREPL;
    }

    public void evalExpression (String s, boolean addToHistory, boolean printToLog, boolean repeatLastREPLEvalIfActive) {
        try {
          if (s.trim().length() > 0) {
                if (printToLog) viewHelpers._("log", this, logPanel, s, inputExprLogType);
                if (evalExpression == null) {
                  viewHelpers._("log", this, logPanel, "Invalid REPL", errLogType);
                } else {
                  final Object ret = evalExpression.invoke(s, addToHistory);

                  if (repeatLastREPLEvalIfActive && autoRepeatLastAction.isChecked()) {
                  final String lastREPLExpr = getLastExpressionSentFromREPL();
                  if (!StringUtils.isBlank(lastREPLExpr)) {
                    new Thread(new Runnable() {
                @Override
                public void run() {
                        if (hasEvalResponseException(ret))
                          return;
                        evalExpression(lastREPLExpr, false, false, false);
                }
              }).start();
                  }
                  }
                }
            }
        } catch (Exception e) {
          if (e instanceof SocketException) {
            connectionLost();
          }
            CCWPlugin.logError(e);
        }
    }

    /**
     * BE CAREFUL: this is a blocking method, waiting for the full
     * nrepl response to come back !
     */
    private boolean hasEvalResponseException(Object evalResponse) {
      ISeq responseSeq = (ISeq) evalResponse;
      while (responseSeq != null) {
        Map<?,?> m = (Map<?,?>) responseSeq.first();
        if (m.containsKey(errorResponseKey)) {
          return true;
        }
        responseSeq = responseSeq.next();
      }
      return false;
    }

    public void printErrorDetail() {
        evalExpression("(require 'clojure.repl)" +
            "(binding [*out* *err*]" +
            "  (if-not *e (println \"No prior exception bound to *e.\")" +
            // concession for Clojure 1.2.0 environments that don't have pst
            "    (if-let [pst (resolve 'clojure.repl/pst)]" +
            "      (pst *e)" +
            "      (.printStackTrace *e *out*))))", false, false, false);
    }

    public void sendInterrupt() {
        viewHelpers._("log", this, logPanel, ";; Interrupting...", inputExprLogType);
        evalExpression.invoke(PersistentHashMap.create("op", "interrupt"), false);
    }

    public void getStdIn () {
        InputDialog dlg = new InputDialog(PlatformUI.getWorkbench().getActiveWorkbenchWindow().getShell(),
                "Input requested", String.format("A REPL expression sent to %s requires a line of *in* input:",
                        interactive.url), "", null);
        // no conditional here; what else would we do if they canceled the dialog?
        // Just a recipe for *requiring* an interrupt...?
        dlg.open();
        evalExpression.invoke(PersistentHashMap.create("op", "stdin", "stdin", dlg.getValue() + "\n"), false);
    }

    /**
     * Echos appropriate content to the log area for an nREPL
     * Response provoked by an expression.
     *
     * @deprecated this should no longer be needed; view_helpers.clj
     * sets up a future that will handle all responses on a REPL's
     * session
     */
    @Deprecated
  public void handleResponse (Response resp, String expression) {
        viewHelpers._("handle-responses", this, logPanel, expression, resp.seq());
    }

    public void closeView () throws Exception {
        IWorkbenchPage page = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage();
        page.hideView(this);
        closeConnections();
    }

    public void closeConnections () throws Exception {
        if (interactive != null) interactive.close();
        if (safeToolConnection != null) safeToolConnection.close();
    }

    public boolean reconnect () throws Exception {
        closeConnections();
        logPanel.append(";; Reconnecting...\n");
        return configure(interactive.url);
    }

    public void setCurrentNamespace (String ns) {
        // TODO waaaay better to put a dropdown namespace chooser in the view's toolbar,
        // and this would just change its selection
      currentNamespace = ns;
      DisplayUtil.asyncExec(new Runnable() {
      @Override public void run() {
        setPartName(String.format("REPL @ %s (%s)", interactive.url, currentNamespace));
      }});
    }

    public String getCurrentNamespace () {
        return currentNamespace;
    }

    private void prepareView () throws Exception {
      // 10s timeout for establishing session (somewhat arbitrary atm)
        sessionId = SafeConnection.safeNewSession(interactive, 10000);
        evalExpression = (IFn) viewHelpers._("configure-repl-view", this, logPanel, interactive.client, sessionId);
    }

    @SuppressWarnings("unchecked")
    public boolean configure (final String url) throws Exception {
        try {
          // Require the drawbridge client to ensure http:// support is started
          if (url.trim().startsWith("http://")) {
            ClojureOSGi.require(CCWPlugin.getDefault().getBundle(), "cemerick.drawbridge.client");
          }

            // TODO - don't need multiple connections anymore, just separate sessions will do.
            interactive = new Connection(url);
            safeToolConnection = new SafeConnection(new Connection(url), this);
            setCurrentNamespace(currentNamespace);
            prepareView();
            final Object clojureVersion = safeToolConnection.send(15000, "op", "eval", "code", "(clojure-version)").values().get(0);
            DisplayUtil.asyncExec(new Runnable() {
        @Override public void run() {
          logPanel.append(";; Clojure " + clojureVersion + "\n");
        }
      });
            NamespaceBrowser.setREPLConnection(safeToolConnection);
            return true;
        } catch (final Exception e) {
            closeView();
            Exception interestingE = e;
            if (!(e instanceof ExceptionInfo) && (e.getCause() instanceof ExceptionInfo)) {
              interestingE = (ExceptionInfo) e.getCause();
            }
            final String title = "REPL Connection error";
            String eMessage = interestingE.getMessage();
            if (StringUtils.isBlank(eMessage)) {
              if (interestingE instanceof TimeoutException) {
                eMessage = "Timeout while waiting for connection";
              }
            }
            final String msg = "Connection to REPL URL " + (url == null ? "<unknown>" : url) + " failed due to " + eMessage;
            CCWPlugin.logWarning(msg, e);
            DisplayUtil.asyncExec(new Runnable() {
        @Override public void run() {
          MessageDialog.openInformation(PlatformUI.getWorkbench().getActiveWorkbenchWindow().getShell(),
              title, msg);
        }
            });
            return false;
        }
    }

    public static REPLView connect (String url, boolean makeActiveREPL) throws Exception {
        return connect(url, null, null, makeActiveREPL);
    }

    public static REPLView connect (final String url, IConsole console, ILaunch launch, final boolean makeActiveREPL) throws Exception {
        String secondaryId;
        final REPLView repl = (REPLView)PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage().showView(
                VIEW_ID
                ,secondaryId = getSecondaryId()
                ,IWorkbenchPage.VIEW_ACTIVATE
                );
        repl.secondaryId = secondaryId;
        repl.console = console;
        repl.showConsoleAction.setEnabled(console != null);
        repl.launch = launch;

        if (repl.configure(url)) {
          if (makeActiveREPL) {
            REPLView.activeREPL.set(repl);
          }
          return repl;
        } else {
          return null;
        }
    }

    public Connection getConnection () {
        return interactive;
    }

    public String getSessionId () {
        return sessionId;
    }

    public Set<String> getAvailableOperations () throws IllegalStateException {
        if (describeInfo == null) {
          try {
              Response r = safeToolConnection.send(10000, "op", "describe");
              // working around the fact that nREPL < 0.2.0-beta9 does *not* send a
              // :done status when an operation is unknown!
              // TODO remove this and just check r.statuses() after we can assume usage
              // of later versions of nREPL
              Object status = ((Map<String, String>)r.seq().first()).get(Keyword.intern("status"));
              if (clojure.lang.Util.equals(status, "unknown-op") || (status instanceof Collection &&
                      ((Collection)status).contains("error"))) {
                  CCWPlugin.logError("Invalid response to \"describe\" request");
                  describeInfo = new HashMap();
              } else {
                  describeInfo = r.combinedResponse();
              }
          } catch (Exception e) {
            CCWPlugin.logError("Error while trying to obtain nrepl available operations", e);
            describeInfo = new HashMap();
          }
        }

        Map<String, Object> ops = (Map<String, Object>)describeInfo.get("ops");
        return ops == null ? new HashSet() : ops.keySet();
    }

    /**
     * Returns the console for the launch that this REPL is associated with, or
     * null if this REPL is using a remote connection.
     */
    public IConsole getConsole () {
        return console;
    }

    public void showConsole() {
        if (console != null) ConsolePlugin.getDefault().getConsoleManager().showConsoleView(console);
    }

    public ILaunch getLaunch() {
        return launch;
    }

    private IPreferenceStore getPreferences() {
      return  CCWPlugin.getDefault().getCombinedPreferenceStore();
    }

    @Override
    public void createPartControl(Composite parent) {
        final IPreferenceStore prefs = getPreferences();

        SashForm split = new SashForm(parent, SWT.VERTICAL);
        split.setBackground(split.getDisplay().getSystemColor(SWT.COLOR_WIDGET_BACKGROUND));


        logPanel = new StyledText(split, SWT.V_SCROLL | SWT.WRAP);
        logPanel.setIndent(4);
        logPanel.setEditable(false);
        logPanel.setFont(JFaceResources.getFont(JFaceResources.TEXT_FONT));

        // Enables logPanel to have same background, etc. colors than clojure
        // editors.
    ClojureSourceViewer.initializeViewerColors(logPanel, prefs, logPanelEditorColors);
    logPanel.addDisposeListener(new DisposeListener() {
      @Override
      public void widgetDisposed(DisposeEvent e) {
        logPanelEditorColors.unconfigure();
      }
    });

        structuralEditionModeStatusContributionItem = ClojureSourceViewer.createStructuralEditionModeStatusContributionItem();
        viewer = new ClojureSourceViewer(split, null, null, false, SWT.V_SCROLL | SWT.H_SCROLL, prefs,
            new ClojureSourceViewer.IStatusLineHandler() {
          @Override
          public StatusLineContributionItem getEditingModeStatusContributionItem() {
            return structuralEditionModeStatusContributionItem;
          }
        }) {
          @Override
      public REPLView getCorrespondingREPL() { return REPLView.this; };
            private SafeConnection getCorrespondingREPLConnection () {
                // we'll be connected by the time this is called
                return safeToolConnection;
            }
            @Override
      public void setStatusLineErrorMessage(String msg) {
              if (msg != null) {
                IStatusLineManager slm = REPLView.this.getViewSite().getActionBars().getStatusLineManager();

                if (slm != null) {
                  slm.setErrorMessage(msg);
                } else {
                  CCWPlugin.logWarning("Could not find status line manager to send the following message from the REPL: " + msg);
                }
              }
            };
            @Override
      public String findDeclaringNamespace() {
              String inline = super.findDeclaringNamespace();
              if (inline != null) {
                return inline;
              } else {
                return currentNamespace;
              }
            };
        };
        viewerConfig = new ClojureSourceViewerConfiguration(prefs, viewer);
        viewer.configure(viewerConfig);

        // Adds support for undo, redo, context information, etc.
        new TextViewerSupport(viewer, getHandlerService());

        getViewSite().setSelectionProvider(viewer);
        viewer.setDocument(ClojureDocumentProvider.configure(new Document()));
        // Display placeholder text when widget is empty and does not have focus
        final StyledText st = (StyledText) viewer.getControl();

        setPlaceHolder(st, "<type clojure code here>");

        viewerWidget = viewer.getTextWidget();

        viewerWidget.setFont(JFaceResources.getFont(JFaceResources.TEXT_FONT));

    // ensure decoration support has been created and configured.
    getSourceViewerDecorationSupport(viewer).install(prefs);

    prefsListener = new IPropertyChangeListener() {
      @Override
      public void propertyChange(PropertyChangeEvent event) {
        initializeLogPanelColors();
      }
    };
    prefs.addPropertyChangeListener(prefsListener);

        // page up/down in input area should control log
        viewerWidget.setKeyBinding(SWT.PAGE_DOWN, SWT.NULL);
        viewerWidget.setKeyBinding(SWT.PAGE_UP, SWT.NULL);
        viewerWidget.addListener(SWT.KeyDown, new Listener () {
           @Override
    public void handleEvent (Event e) {
               switch (e.keyCode) {
                   case SWT.PAGE_DOWN:
                       logPanel.invokeAction(ST.PAGE_DOWN);
                       break;
                   case SWT.PAGE_UP:
                       logPanel.invokeAction(ST.PAGE_UP);
                       break;
               }
           }
        });

        installMessageDisplayer(viewerWidget, new MessageProvider() {
      @Override
      public String getMessageText() {
        return getEvaluationHint();
      }
    });

        installAutoEvalExpressionOnEnter();

        installEvalTopLevelSExpressionCommand();

        /*
         * Need to hook up here to force a re-evaluation of the preferences
         * for the syntax coloring, after the token scanner has been
         * initialized. Otherwise the very first Clojure editor will not
         * have any tokens colored.
         * TODO this is repeated in ClojureEditor...surely we can make the source viewer self-sufficient here
         */
        viewer.propertyChange(null);

        viewerWidget.addFocusListener(new NamespaceRefreshFocusListener());
        logPanel.addFocusListener(new NamespaceRefreshFocusListener());

        split.setWeights(new int[] {100, 75});

        getViewSite().getActionBars().getStatusLineManager().add(this.structuralEditionModeStatusContributionItem);
        viewer.updateStructuralEditingModeStatusField();
        structuralEditionModeStatusContributionItem.setActionHandler(new Action() {
        @Override
      public void run() {
        viewer.toggleStructuralEditionMode();
      }
      });


        resetFont();
        JFaceResources.getFontRegistry().addListener(fontChangeListener);
        parent.addDisposeListener(new DisposeListener() {
      @Override
      public void widgetDisposed(DisposeEvent e) {
        CCWPlugin.getTracer().trace(TraceOptions.REPL, "REPLView ", REPLView.this.secondaryId, " parent composite disposed");
        JFaceResources.getFontRegistry().removeListener(fontChangeListener);
        prefs.removePropertyChangeListener(prefsListener);
        activeREPL.compareAndSet(REPLView.this, null);
      }
    });

        /*
         * There's a BUG starting with Eclipse Juno 4.2.2 with the compatibility
         * layer and the context mgt / toolbar icons mgt (they just don't work).
         * Reverting back from Command+Handler+MenuItems to Actions works,
         * so that's what we're doing here.
         */
        createActions();
        createToolbar();

        /*
         * TODO find a way for the following code line to really work. That is add
         * the necessary additional code for enabling "handlers" (in fact, my fear
         * is that those really are not handlers but "actions" that will need to be
         * manually enabled as I did above for EVALUATE_TOP_LEVEL_S_EXPRESSION :-( )
         */
        activate("org.eclipse.ui.textEditorScope");

        /* Thought just activating CCW_UI_CONTEXT_REPL would also activate its parent contexts
         * but apparently not, so here we activate explicitly all the contexts we want (FIXME?)
         */
        activate(IClojureEditor.KEY_BINDING_SCOPE);
        activate(CCW_UI_CONTEXT_REPL);
    }

  private void setPlaceHolder(final StyledText st, final String placeholder) {
    final IPropertyChangeListener replHintsListener = new IPropertyChangeListener() {
      @Override public void propertyChange(PropertyChangeEvent event) {
        if (event.getProperty().equals(PreferenceConstants.REPL_VIEW_DISPLAY_HINTS)) {
          updatePlaceHolder(st, placeholder);
        }
      }
    };
    getPreferences().addPropertyChangeListener(replHintsListener);
    st.addDisposeListener(new DisposeListener() {
      @Override public void widgetDisposed(DisposeEvent e) {
        getPreferences().removePropertyChangeListener(replHintsListener);
      }
    });
    updatePlaceHolder(st, placeholder);
  }

  private void updatePlaceHolder(final StyledText st, final String placeholder) {
    if (getPreferences().getBoolean(PreferenceConstants.REPL_VIEW_DISPLAY_HINTS)) {
      if (this.placeholder == null) {
        this.placeholder = new StyledTextPlaceHolder(st, placeholder);
      }
      this.placeholder.setPlaceHolder(placeholder);
    } else {
      if (this.placeholder != null) {
        this.placeholder.setPlaceHolder(null);
      }
    }
  }

  private static class StyledTextPlaceHolder {
    private final StyledText st;
    private String placeholder;
    private boolean hasFocus;
    private boolean isPlaceholderDisplayed;

    public StyledTextPlaceHolder(final StyledText st, String p) {
      this.st = st;
      this.placeholder = p;
      this.hasFocus = st.isFocusControl();

      renderPlaceholder();

      st.addFocusListener(new FocusListener() {
        @Override public void focusLost(FocusEvent e) {
          hasFocus = false;
          renderPlaceholder();
        }
        @Override public void focusGained(FocusEvent e) {
          hasFocus = true;
          renderPlaceholder();
        }
      });
    }

    private void renderPlaceholder() {
      if (hasFocus) {
        if (isPlaceholderDisplayed) {
          isPlaceholderDisplayed = false;
          st.setText("");
          st.setStyleRange(null);
        }
      } else {
        if (isPlaceholderDisplayed || StringUtils.isEmpty(st.getText())) {
          isPlaceholderDisplayed = true;
          st.setText(StringUtils.safeString(placeholder));
          if (!StringUtils.isEmpty(placeholder)) {
            st.setStyleRange(new StyleRange(0, placeholder.length(), null, null, SWT.ITALIC));
          }
        }
      }
    }

    public void setPlaceHolder(final String placeholder) {
      this.placeholder = placeholder;
      renderPlaceholder();
    }

  }

    private Action autoRepeatLastAction;
    private Action printErrorAction;
    private Action interruptAction;
    private Action reconnectAction;
    private Action clearLogAction;
    private Action newSessionAction;
    private Action showConsoleAction;

  private void createActions() {

    autoRepeatLastAction = new Action("Repeat last evaluation each time editor sends changes", Action.AS_CHECK_BOX) {
      @Override
      public void run() {
        // Nothing to do, only the button state is relevant
      }
    };
    autoRepeatLastAction.setToolTipText("Repeat last evaluation each time editor sends changes");
    autoRepeatLastAction.setImageDescriptor(getImageDescriptor("repl/arrow-repeat-once_16x16.png"));

    printErrorAction = new Action("Print detailed output from the last exception") {
      @Override
      public void run() {
        printErrorDetail();
      }
    };
    printErrorAction.setToolTipText("Print detailed output from the last exception");
    printErrorAction
        .setImageDescriptor(getImageDescriptor("repl/print_last_error.gif"));


    interruptAction = new Action("Interrupt Evaluation") {
      @Override
      public void run() {
        sendInterrupt();
      }
    };
    interruptAction.setToolTipText("Forcibly interrupt the currently-running evaluation for this session.");
    interruptAction
        .setImageDescriptor(getImageDescriptor("repl/interrupt.gif"));


    reconnectAction = new Action("Reconnect") {
      @Override
      public void run() {
              try {
                  reconnect();
              } catch (Exception e) {
                final String MSG = "Unexpected exception occured while trying to reconnect REPL view to clojure server";
                ErrorDialog.openError(
                    REPLView.this.getSite().getShell(),
                    "Reconnection Error",
                    MSG,
                    CCWPlugin.createErrorStatus(MSG, e));
              }
          }
    };
    reconnectAction.setToolTipText("Reconnect to this REPL's host and port (first disconnecting if necessary)");
    reconnectAction
        .setImageDescriptor(getImageDescriptor("repl/reconnect.gif"));


    clearLogAction = new Action("Clear REPL log") {
      @Override
      public void run() {
        logPanel.setText("");
      }
    };
    clearLogAction.setToolTipText("Clear the REPL&apos;s log");
    clearLogAction.setImageDescriptor(getImageDescriptor("repl/clear.gif"));


    newSessionAction = new Action("New Session") {
      @Override
      public void run() {
              try {
                  REPLView.connect(getConnection().url, true);
              } catch (Exception e) {
                  final String msg = "Unexpected exception occured while trying to connect REPL view to clojure server";
                  ErrorDialog.openError(
                    REPLView.this.getSite().getShell(),
                          "Connection Error",
                          msg,
                          CCWPlugin.createErrorStatus(msg, e));
              }
      }
    };
    newSessionAction.setToolTipText("Open a new REPL session connected to this REPL's Clojure process.");
    newSessionAction
        .setImageDescriptor(getImageDescriptor("repl/new_wiz.gif"));


    showConsoleAction = new Action("Show Console") {
      @Override
      public void run() {
        showConsole();
      }
    };
    showConsoleAction.setToolTipText("Show the console for the JVM process to which this REPL is connected");
    showConsoleAction.setDisabledImageDescriptor(getImageDescriptor("repl/console_disabled.gif"));
    showConsoleAction
        .setImageDescriptor(getImageDescriptor("repl/console.gif"));


  }

    private void createToolbar() {
      IToolBarManager mgr = getViewSite().getActionBars().getToolBarManager();
      mgr.add(autoRepeatLastAction);
      mgr.add(printErrorAction);
      mgr.add(interruptAction);
      mgr.add(reconnectAction);
      mgr.add(clearLogAction);
      mgr.add(newSessionAction);
      mgr.add(showConsoleAction);
    }

    /**
     * Returns the image descriptor with the given relative path.
     */
    private ImageDescriptor getImageDescriptor(String relativePath) {
            String iconPath = "icons/";
            try {
                    CCWPlugin plugin = CCWPlugin.getDefault();
                    URL installURL = plugin.getDescriptor().getInstallURL();
                    URL url = new URL(installURL, iconPath + relativePath);
                    return ImageDescriptor.createFromURL(url);
            }
            catch (MalformedURLException e) {
                    // should not happen
                    return ImageDescriptor.getMissingImageDescriptor();
            }
    }

    private void activate(String contextId) {
        ((IContextService)getSite().getService(IContextService.class)).activateContext(contextId);

    }

    private void initializeLogPanelColors() {
    ClojureSourceViewer.initializeViewerColors(logPanel, getPreferences(), logPanelEditorColors);

    }

    private interface MessageProvider {
      String getMessageText();
    }

    private void installMessageDisplayer(final StyledText textViewer, final MessageProvider hintProvider) {
    textViewer.addListener(SWT.Paint, new Listener() {
        private int getScrollbarAdjustment () {
            if (!Platform.getOS().equals(Platform.OS_MACOSX)) return 0;

            // You cannot reliably determine if a scrollbar is visible or not
                //   (http://stackoverflow.com/questions/5674207)
                // So, we need to determine if the vertical scrollbar is "needed" based
                // on the contents in the viewer
                Rectangle clientArea = textViewer.getClientArea();
                int textLength = textViewer.getText().length();
                if (textLength == 0 || textViewer.getTextBounds(0, Math.max(0, textLength - 1)).height < clientArea.height) {
                    return textViewer.getVerticalBar().getSize().x;
                } else {
                    return 0;
                }
        }

      @Override
      public void handleEvent(Event event) {
        String message = hintProvider.getMessageText();
        if (message == null)
          return;

                // keep the 'tooltip' using the default font
                event.gc.setFont(JFaceResources.getFont(JFaceResources.DEFAULT_FONT));

        Point topRightPoint = topRightPoint(textViewer.getClientArea());
        int sWidth = textWidthPixels(message, event);
        int x = Math.max(topRightPoint.x - sWidth + getScrollbarAdjustment(), 0);
        int y = topRightPoint.y;

        // the text widget doesn't know we're painting the hint, so it won't necessarily
        // clear old presentations of it; this leads to streaking on Windows if we don't
        // clear the foreground explicitly
        Color fg = event.gc.getForeground();
        event.gc.setForeground(event.gc.getBackground());
        event.gc.drawRectangle(textViewer.getClientArea());
        event.gc.setForeground(fg);
        event.gc.setAlpha(200);
        event.gc.drawText(message, x, y, true);
      }

      private Point topRightPoint(Rectangle clipping) {
        return new Point(clipping.x + clipping.width, clipping.y);
      }

      private int textWidthPixels(String text, Event evt) {
        int width = 0;
        for (int i = 0; i < text.length(); i++) {
          width += evt.gc.getAdvanceWidth(text.charAt(i));
        }
        return width;
      }
    });
    }

    private String getEvaluationHint() {
      if (!getPreferences().getBoolean(PreferenceConstants.REPL_VIEW_DISPLAY_HINTS))
        return null;

      if (getPreferences().getBoolean(PreferenceConstants.REPL_VIEW_AUTO_EVAL_ON_ENTER_ACTIVE)) {
        return Messages.REPLView_autoEval_on_Enter_active;
      } else {
        return Messages.format(Messages.REPLView_autoEval_on_Enter_inactive,
            Platform.getOS().equals(Platform.OS_MACOSX)
              ? "Cmd"
                : "Ctrl");
      }
    }

    private void installAutoEvalExpressionOnEnter() {
        viewerWidget.addVerifyKeyListener(new VerifyKeyListener() {
          private boolean enterAlonePressed(VerifyEvent e) {
            return (e.keyCode == SWT.LF || e.keyCode == SWT.CR)
            && e.stateMask == SWT.NONE;
          }
          private boolean noSelection() {
            return viewerWidget.getSelectionCount() == 0;
          }
          private String textAfterCaret() {
            return viewerWidget.getText().substring(
                viewerWidget.getSelection().x);
          }
          private boolean isAutoEvalOnEnterAllowed() {
            return getPreferences().getBoolean(PreferenceConstants.REPL_VIEW_AUTO_EVAL_ON_ENTER_ACTIVE);
          }
      @Override
      public void verifyKey(VerifyEvent e) {
        if (    isAutoEvalOnEnterAllowed()
            && enterAlonePressed(e)
            && noSelection()
            && textAfterCaret().trim().isEmpty()
            && !viewer.isParseTreeBroken()) {

          final String widgetText = viewerWidget.getText();

          // Executing evalExpression() via SWT's asyncExec mechanism,
          // we ensure all the normal behaviour is done by the Eclipse
          // framework on the Enter key, before sending the code.
          // For example, we are then able to get rid of a bug with
          // the content assistant which ensures the text is completed
          // with the selection before being sent for evaluation.
          DisplayUtil.asyncExec(new Runnable() {
            @Override
            public void run() {
              // we do not execute auto eval if some non-blank text has
              // been added between the check and the execution
              final String text = viewerWidget.getText();
              int idx = text.indexOf(widgetText);
              if (idx == 0 && text.substring(widgetText.length()).trim().isEmpty()) {
                evalExpression();
              }
            }});
        }
      }
        });
    }

    IHandlerService getHandlerService() {
        return (IHandlerService) getViewSite().getService(IHandlerService.class);
    }

    private void installEvalTopLevelSExpressionCommand() {
        getHandlerService().activateHandler(IClojureEditorActionDefinitionIds.EVALUATE_TOP_LEVEL_S_EXPRESSION, new AbstractHandler() {
        @Override
      public Object execute(ExecutionEvent event) throws ExecutionException {
          evalExpression();
          return null;
        }
      });
    }

    /**
     * This exists solely to work around what can only be considered a bug in Eclipse starting
     * with Juno (I20120608-1400 FWIW), where views that can have multiple simultaneous instances
     * (of which REPLView is one) cannot share activated contexts.  This means that actions
     * bound to a given context will appear as active in one view, but not in another.
     */
    private class ViewContextActivationListener implements FocusListener {
        private List<IContextActivation> activations = new ArrayList();

        @Override
    public void focusLost(FocusEvent e) {
            //for (IContextActivation activation : activations) {
            //    ((IContextService)REPLView.this.getSite().getService(IContextService.class)).deactivateContext(activation);
            //}
            activations = new ArrayList();
        }

        private void activate (String contextId) {
            activations.add(((IContextService)getSite().getService(IContextService.class)).activateContext(contextId));
        }

        @Override
    public void focusGained(FocusEvent e) {
            /*
             * TODO find a way for the following code line to really work. That is add
             * the necessary additional code for enabling "handlers" (in fact, my fear
             * is that those really are not handlers but "actions" that will need to be
             * manually enabled as I did above for EVALUATE_TOP_LEVEL_S_EXPRESSION :-( )
             */
            activate("org.eclipse.ui.textEditorScope");

            /* Thought just activating CCW_UI_CONTEXT_REPL would also activate its parent contexts
             * but apparently not, so here we activate explicitly all the contexts we want (FIXME?)
             */
            activate(IClojureEditor.KEY_BINDING_SCOPE);
            activate(CCW_UI_CONTEXT_REPL);
        }
    }

  /**
   * Returns the source viewer decoration support.
   *
   * @param viewer the viewer for which to return a decoration support
   * @return the source viewer decoration support
   */
    // From ClojureEditor + AbstractDecoratedTextEditor ...
  protected SourceViewerDecorationSupport getSourceViewerDecorationSupport(ISourceViewer viewer) {
    if (fSourceViewerDecorationSupport == null) {
      fSourceViewerDecorationSupport= new SourceViewerDecorationSupport(
          viewer,
          null/*getOverviewRuler()*/,
          null/*getAnnotationAccess()*/,
          EditorsPlugin.getDefault().getSharedTextColors()/*getSharedColors()*/
          );
      editorSupport._("configureSourceViewerDecorationSupport",
          fSourceViewerDecorationSupport, viewer);
    }
    return fSourceViewerDecorationSupport;
  }

    @Override
    public void dispose() {
        super.dispose();

        if (secondaryId != null) {
          releaseSecondaryId(secondaryId);
        }

        fSourceViewerDecorationSupport = (SourceViewerDecorationSupport) editorSupport._("disposeSourceViewerDecorationSupport",
            fSourceViewerDecorationSupport);
        try {
            if (interactive != null) interactive.close();
            if (safeToolConnection != null) safeToolConnection.close();

            if (launch != null && launch.canTerminate()) {
              try {
          launch.terminate();
        } catch (DebugException e) {
          e.printStackTrace();
        }
            }

        } catch (IOException e) {
            CCWPlugin.logError(e);
        }
    }

    public boolean isDisposed () {
        // TODO we actually want to report whether the viewpart has been closed, not whether or not
        // the platform has disposed the widget peer
        return viewerWidget.isDisposed();
    }

    @Override
    public void setFocus() {
        viewerWidget.setFocus();
    }

    private final class NamespaceRefreshFocusListener implements FocusListener {
        @Override
    public void focusGained(FocusEvent e) {
          if (isConnectionLost) {
            NamespaceBrowser.setREPLConnection(null);
          } else {
            activeREPL.set(REPLView.this);
            NamespaceBrowser.setREPLConnection(safeToolConnection);
          }
        }

        @Override
    public void focusLost(FocusEvent e) {}
    }

    @Override
    public Object getAdapter(Class adapter) {
      if (adapter == IClojureEditor.class) {
        return viewer;
      } else {
        return super.getAdapter(adapter);
      }
    }

}
TOP

Related Classes of ccw.repl.REPLView$NamespaceRefreshFocusListener

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.