/*
* AceEditorWidget.java
*
* Copyright (C) 2009-12 by RStudio, Inc.
*
* Unless you have received this program directly from RStudio pursuant
* to the terms of a commercial license agreement with RStudio, then
* this program is licensed to you under the terms of version 3 of the
* GNU Affero General Public License. This program is distributed WITHOUT
* ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT,
* MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the
* AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details.
*
*/
package org.rstudio.studio.client.workbench.views.source.editors.text;
import java.util.ArrayList;
import java.util.List;
import com.google.gwt.core.client.JsArray;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.RepeatingCommand;
import com.google.gwt.core.client.Scheduler.ScheduledCommand;
import com.google.gwt.dom.client.NativeEvent;
import com.google.gwt.dom.client.Element;
import com.google.gwt.event.dom.client.*;
import com.google.gwt.event.logical.shared.AttachEvent;
import com.google.gwt.event.logical.shared.HasValueChangeHandlers;
import com.google.gwt.event.logical.shared.ValueChangeEvent;
import com.google.gwt.event.logical.shared.ValueChangeHandler;
import com.google.gwt.event.shared.HandlerManager;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.event.shared.HasHandlers;
import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.HTML;
import com.google.gwt.user.client.ui.RequiresResize;
import com.google.inject.Inject;
import org.rstudio.core.client.BrowseCap;
import org.rstudio.core.client.CommandWithArg;
import org.rstudio.core.client.Debug;
import org.rstudio.core.client.StringUtil;
import org.rstudio.core.client.widget.FontSizer;
import org.rstudio.core.client.widget.events.SelectionChangedEvent;
import org.rstudio.studio.client.RStudioGinjector;
import org.rstudio.studio.client.application.events.EventBus;
import org.rstudio.studio.client.common.Value;
import org.rstudio.studio.client.common.debugging.model.Breakpoint;
import org.rstudio.studio.client.events.*;
import org.rstudio.studio.client.server.Void;
import org.rstudio.studio.client.workbench.commands.Commands;
import org.rstudio.studio.client.workbench.commands.RStudioCommandExecutedFromShortcutEvent;
import org.rstudio.studio.client.workbench.views.output.lint.LintResources;
import org.rstudio.studio.client.workbench.views.output.lint.model.AceAnnotation;
import org.rstudio.studio.client.workbench.views.output.lint.model.LintItem;
import org.rstudio.studio.client.workbench.views.source.editors.text.ace.AceClickEvent;
import org.rstudio.studio.client.workbench.views.source.editors.text.ace.AceDocumentChangeEventNative;
import org.rstudio.studio.client.workbench.views.source.editors.text.ace.AceEditorNative;
import org.rstudio.studio.client.workbench.views.source.editors.text.ace.AceMouseEventNative;
import org.rstudio.studio.client.workbench.views.source.editors.text.ace.AceMouseMoveEvent;
import org.rstudio.studio.client.workbench.views.source.editors.text.ace.Anchor;
import org.rstudio.studio.client.workbench.views.source.editors.text.ace.AnchoredRange;
import org.rstudio.studio.client.workbench.views.source.editors.text.ace.LineWidgetManager;
import org.rstudio.studio.client.workbench.views.source.editors.text.ace.Marker;
import org.rstudio.studio.client.workbench.views.source.editors.text.ace.Position;
import org.rstudio.studio.client.workbench.views.source.editors.text.ace.Range;
import org.rstudio.studio.client.workbench.views.source.editors.text.ace.events.AfterAceRenderEvent;
import org.rstudio.studio.client.workbench.views.source.editors.text.events.*;
import org.rstudio.studio.client.workbench.views.source.editors.text.events.FoldChangeEvent.Handler;
public class AceEditorWidget extends Composite
implements RequiresResize,
HasValueChangeHandlers<Void>,
HasFoldChangeHandlers,
HasAllKeyHandlers,
EditEvent.Handler
{
public AceEditorWidget()
{
this(true);
}
public AceEditorWidget(boolean applyNormalFontSize)
{
RStudioGinjector.INSTANCE.injectMembers(this);
initWidget(new HTML());
if (applyNormalFontSize)
FontSizer.applyNormalFontSize(this);
setSize("100%", "100%");
capturingHandlers_ = new HandlerManager(this);
addEventListener(getElement(), "keydown", capturingHandlers_);
addEventListener(getElement(), "keyup", capturingHandlers_);
addEventListener(getElement(), "keypress", capturingHandlers_);
addStyleName("loading");
editor_ = AceEditorNative.createEditor(getElement());
editor_.manageDefaultKeybindings();
editor_.getRenderer().setHScrollBarAlwaysVisible(false);
editor_.setShowPrintMargin(false);
editor_.setPrintMarginColumn(0);
editor_.setHighlightActiveLine(false);
editor_.setHighlightGutterLine(false);
editor_.setFixedWidthGutter(true);
editor_.delegateEventsTo(AceEditorWidget.this);
editor_.onChange(new CommandWithArg<AceDocumentChangeEventNative>()
{
public void execute(AceDocumentChangeEventNative event)
{
// Case 3815: It appears to be possible for change events to be
// fired recursively, which exhausts the stack. This shouldn't
// happen, but since it has in at least one setting, guard against
// recursion here.
if (inOnChangeHandler_)
{
Debug.log("Warning: ignoring recursive ACE change event");
return;
}
inOnChangeHandler_ = true;
try
{
ValueChangeEvent.fire(AceEditorWidget.this, null);
AceEditorWidget.this.fireEvent(new DocumentChangedEvent(event));
updateBreakpoints(event);
updateAnnotations(event);
// Immediately re-render on change if we have markers, to
// ensure they're re-drawn in the correct locations.
if (editor_.getSession().getMarkers(true).size() > 0)
{
Scheduler.get().scheduleDeferred(new ScheduledCommand()
{
@Override
public void execute()
{
editor_.getRenderer().renderMarkers();
}
});
}
}
catch (Exception ex)
{
Debug.log("Exception occurred during ACE change event: " +
ex.getMessage());
}
inOnChangeHandler_ = false;
}
});
editor_.onChangeFold(new Command()
{
@Override
public void execute()
{
fireEvent(new FoldChangeEvent());
}
});
editor_.onGutterMouseDown(new CommandWithArg<AceMouseEventNative>()
{
@Override
public void execute(AceMouseEventNative arg)
{
// make sure the click is actually intended for the gutter
com.google.gwt.dom.client.Element targetElement =
Element.as(arg.getNativeEvent().getEventTarget());
if (targetElement.getClassName().indexOf("ace_gutter-cell") < 0)
{
return;
}
NativeEvent evt = arg.getNativeEvent();
// right-clicking shouldn't set a breakpoint
if (evt.getButton() != NativeEvent.BUTTON_LEFT)
{
return;
}
// make sure that the click was in the left half of the element--
// clicking on the line number itself (or the gutter near the
// text) shouldn't set a breakpoint.
if (evt.getClientX() <
(targetElement.getAbsoluteLeft() +
(targetElement.getClientWidth() / 2)))
{
toggleBreakpointAtPosition(arg.getDocumentPosition());
}
}
});
editor_.getSession().getSelection().addCursorChangeHandler(new CommandWithArg<Position>()
{
public void execute(Position arg)
{
AceEditorWidget.this.fireEvent(new CursorChangedEvent(arg));
}
});
aceEventHandlers_ = new ArrayList<HandlerRegistration>();
aceEventHandlers_.add(AceEditorNative.addEventListener(
editor_,
"undo",
new CommandWithArg<Void>()
{
public void execute(Void arg)
{
fireEvent(new UndoRedoEvent(false));
}
}));
aceEventHandlers_.add(AceEditorNative.addEventListener(
editor_,
"redo",
new CommandWithArg<Void>()
{
public void execute(Void arg)
{
fireEvent(new UndoRedoEvent(true));
}
}));
aceEventHandlers_.add(AceEditorNative.addEventListener(
editor_,
"paste",
new CommandWithArg<String>()
{
public void execute(String text)
{
fireEvent(new PasteEvent(text));
}
}));
aceEventHandlers_.add(AceEditorNative.addEventListener(
editor_,
"mousemove",
new CommandWithArg<AceMouseEventNative>()
{
@Override
public void execute(AceMouseEventNative event)
{
fireEvent(new AceMouseMoveEvent(event));
}
}));
aceEventHandlers_.add(AceEditorNative.addEventListener(
editor_,
"mousedown",
new CommandWithArg<AceMouseEventNative>()
{
@Override
public void execute(AceMouseEventNative event)
{
fireEvent(new AceClickEvent(event));
}
}));
aceEventHandlers_.add(AceEditorNative.addEventListener(
editor_.getRenderer(),
"afterRender",
new CommandWithArg<Void>()
{
@Override
public void execute(Void event)
{
fireEvent(new RenderFinishedEvent());
isRendered_ = true;
events_.fireEvent(new AfterAceRenderEvent(AceEditorWidget.this.getEditor()));
}
}));
aceEventHandlers_.add(AceEditorNative.addEventListener(
editor_,
"changeSelection",
new CommandWithArg<Void>()
{
@Override
public void execute(Void event)
{
fireEvent(new AceSelectionChangedEvent());
}
}));
addAttachHandler(new AttachEvent.Handler()
{
@Override
public void onAttachOrDetach(AttachEvent event)
{
if (!event.isAttached())
{
for (HandlerRegistration registration : aceEventHandlers_)
registration.removeHandler();
aceEventHandlers_.clear();
}
}
});
if (!hasEditHandlers_)
{
events_.addHandler(EditEvent.TYPE, this);
hasEditHandlers_ = true;
}
events_.addHandler(
RStudioCommandExecutedFromShortcutEvent.TYPE,
new RStudioCommandExecutedFromShortcutEvent.Handler()
{
@Override
public void onRStudioCommandExecutedFromShortcut(RStudioCommandExecutedFromShortcutEvent event)
{
clearKeyBuffers(editor_);
}
});
}
// When the 'keyBinding' field is initialized (the field holding all keyboard
// handlers for an Ace editor), an associated '$data' element is used to store
// information on keys (to allow for keyboard chaining, and so on). We refresh
// that data whenever an RStudio AppCommand is executed (thereby ensuring that
// the current keybuffer is cleared as far as Ace is concerned)
private static final native void clearKeyBuffers(AceEditorNative editor) /*-{
var keyBinding = editor.keyBinding;
keyBinding.$data = {editor: editor};
}-*/;
public void onEdit(EditEvent edit)
{
if (edit.isBeforeEdit())
unmapForEdit(edit.getType());
else
remapForEdit(edit.getType());
}
private final void unmapForEdit(int type)
{
if (type == EditEvent.TYPE_COPY)
unmapForEditImpl("<C-c>", "c-c");
else if (type == EditEvent.TYPE_CUT)
unmapForEditImpl("<C-x>", "c-x");
else if (type == EditEvent.TYPE_PASTE)
unmapForEditImpl("<C-v>", "c-v");
}
private final void remapForEdit(int type)
{
if (type == EditEvent.TYPE_COPY)
remapForEditImpl("<C-c>", "c-c");
else if (type == EditEvent.TYPE_CUT)
remapForEditImpl("<C-x>", "c-x");
else if (type == EditEvent.TYPE_PASTE)
remapForEditImpl("<C-v>", "c-v");
}
private static final native void unmapForEditImpl(String vimKeys, String emacsKeys)
/*-{
// Handle Vim mapping
var Vim = $wnd.require("ace/keyboard/vim").handler;
var keymap = Vim.defaultKeymap;
for (var i = 0; i < keymap.length; i++) {
if (keymap[i].keys === vimKeys) {
keymap[i].keys = "DISABLED:" + vimKeys;
break;
}
}
// Handle Emacs mapping
var Emacs = $wnd.require("ace/keyboard/emacs").handler;
var bindings = Emacs.commandKeyBinding;
bindings["DISABLED:" + emacsKeys] = bindings[emacsKeys];
delete bindings[emacsKeys];
}-*/;
private static final native void remapForEditImpl(String vimKeys, String emacsKeys)
/*-{
// Handle Vim mapping
var Vim = $wnd.require("ace/keyboard/vim").handler;
var keymap = Vim.defaultKeymap;
for (var i = 0; i < keymap.length; i++) {
if (keymap[i].keys === "DISABLED:" + vimKeys) {
keymap[i].keys = vimKeys;
break;
}
}
// Handle Emacs mapping
var Emacs = $wnd.require("ace/keyboard/emacs").handler;
var bindings = Emacs.commandKeyBinding;
if (bindings["DISABLED:" + emacsKeys] != null) {
bindings[emacsKeys] = bindings["DISABLED:" + emacsKeys];
delete bindings["DISABLED:" + emacsKeys];
}
}-*/;
@Inject
private void initialize(EventBus events)
{
events_ = events;
}
public HandlerRegistration addCursorChangedHandler(
CursorChangedHandler handler)
{
return addHandler(handler, CursorChangedEvent.TYPE);
}
@Override
public HandlerRegistration addFoldChangeHandler(Handler handler)
{
return addHandler(handler, FoldChangeEvent.TYPE);
}
public HandlerRegistration addBreakpointSetHandler
(BreakpointSetEvent.Handler handler)
{
return addHandler(handler, BreakpointSetEvent.TYPE);
}
public HandlerRegistration addBreakpointMoveHandler
(BreakpointMoveEvent.Handler handler)
{
return addHandler(handler, BreakpointMoveEvent.TYPE);
}
public void toggleBreakpointAtCursor()
{
Position pos = editor_.getSession().getSelection().getCursor();
toggleBreakpointAtPosition(Position.create(pos.getRow(), 0));
}
public AceEditorNative getEditor() {
return editor_;
}
@Override
protected void onLoad()
{
super.onLoad();
editor_.getRenderer().updateFontSize();
onResize();
fireEvent(new EditorLoadedEvent(editor_));
events_.fireEvent(new EditorLoadedEvent(editor_));
int delayMs = initToEmptyString_ ? 100 : 500;
// On Windows desktop sometimes we inexplicably end up at the wrong size
// if the editor is being resized while it's loading (such as when a new
// document is created while the source pane is hidden)
Scheduler.get().scheduleFixedDelay(new RepeatingCommand()
{
public boolean execute()
{
if (isAttached())
onResize();
removeStyleName("loading");
return false;
}
}, delayMs);
}
public void onResize()
{
editor_.resize();
}
public void onActivate()
{
if (editor_ != null)
{
if (BrowseCap.INSTANCE.aceVerticalScrollBarIssue())
editor_.getRenderer().forceScrollbarUpdate();
editor_.getRenderer().updateFontSize();
editor_.getRenderer().forceImmediateRender();
}
}
public void setCode(String code)
{
code = StringUtil.notNull(code);
initToEmptyString_ = code.length() == 0;
editor_.getSession().setValue(code);
}
public HandlerRegistration addValueChangeHandler(ValueChangeHandler<Void> handler)
{
return addHandler(handler, ValueChangeEvent.getType());
}
public HandlerRegistration addFocusHandler(FocusHandler handler)
{
return addHandler(handler, FocusEvent.getType());
}
public HandlerRegistration addBlurHandler(BlurHandler handler)
{
return addHandler(handler, BlurEvent.getType());
}
public HandlerRegistration addMouseDownHandler(MouseDownHandler handler)
{
return addDomHandler(handler, MouseDownEvent.getType());
}
public HandlerRegistration addMouseMoveHandler(MouseMoveHandler handler)
{
return addDomHandler(handler, MouseMoveEvent.getType());
}
public HandlerRegistration addMouseUpHandler(MouseUpHandler handler)
{
return addDomHandler(handler, MouseUpEvent.getType());
}
public HandlerRegistration addClickHandler(ClickHandler handler)
{
return addDomHandler(handler, ClickEvent.getType());
}
public HandlerRegistration addEditorLoadedHandler(EditorLoadedHandler handler)
{
return addHandler(handler, EditorLoadedEvent.TYPE);
}
public HandlerRegistration addKeyDownHandler(KeyDownHandler handler)
{
return addHandler(handler, KeyDownEvent.getType());
}
public HandlerRegistration addKeyPressHandler(KeyPressHandler handler)
{
return addHandler(handler, KeyPressEvent.getType());
}
public HandlerRegistration addKeyUpHandler(KeyUpHandler handler)
{
return addHandler(handler, KeyUpEvent.getType());
}
public HandlerRegistration addCapturingKeyDownHandler(KeyDownHandler handler)
{
return capturingHandlers_.addHandler(KeyDownEvent.getType(), handler);
}
public HandlerRegistration addCapturingKeyPressHandler(KeyPressHandler handler)
{
return capturingHandlers_.addHandler(KeyPressEvent.getType(), handler);
}
public HandlerRegistration addCapturingKeyUpHandler(KeyUpHandler handler)
{
return capturingHandlers_.addHandler(KeyUpEvent.getType(), handler);
}
private static native void addEventListener(Element element,
String event,
HasHandlers handlers) /*-{
var listener = $entry(function(e) {
@com.google.gwt.event.dom.client.DomEvent::fireNativeEvent(Lcom/google/gwt/dom/client/NativeEvent;Lcom/google/gwt/event/shared/HasHandlers;Lcom/google/gwt/dom/client/Element;)(e, handlers, element);
});
element.addEventListener(event, listener, true);
}-*/;
public HandlerRegistration addUndoRedoHandler(UndoRedoHandler handler)
{
return addHandler(handler, UndoRedoEvent.TYPE);
}
public HandlerRegistration addPasteHandler(PasteEvent.Handler handler)
{
return addHandler(handler, PasteEvent.TYPE);
}
public HandlerRegistration addAceMouseMoveHandler(AceMouseMoveEvent.Handler handler)
{
return addHandler(handler, AceMouseMoveEvent.TYPE);
}
public HandlerRegistration addAceClickHandler(AceClickEvent.Handler handler)
{
return addHandler(handler, AceClickEvent.TYPE);
}
public HandlerRegistration addSelectionChangedHandler(AceSelectionChangedEvent.Handler handler)
{
return addHandler(handler, AceSelectionChangedEvent.TYPE);
}
public void forceResize()
{
editor_.getRenderer().onResize(true);
}
public void autoHeight()
{
editor_.autoHeight();
}
public void forceCursorChange()
{
editor_.onCursorChange();
}
public void addOrUpdateBreakpoint(Breakpoint breakpoint)
{
int idx = getBreakpointIdxById(breakpoint.getBreakpointId());
if (idx >= 0)
{
removeBreakpointMarker(breakpoint);
breakpoint.setEditorState(breakpoint.getState());
breakpoint.setEditorLineNumber(breakpoint.getLineNumber());
}
else
{
breakpoints_.add(breakpoint);
}
placeBreakpointMarker(breakpoint);
}
public void removeBreakpoint(Breakpoint breakpoint)
{
int idx = getBreakpointIdxById(breakpoint.getBreakpointId());
if (idx >= 0)
{
removeBreakpointMarker(breakpoint);
breakpoints_.remove(idx);
}
}
public void removeAllBreakpoints()
{
for (Breakpoint breakpoint: breakpoints_)
{
removeBreakpointMarker(breakpoint);
}
breakpoints_.clear();
}
public void setChunkLineExecState(int start, int end, int state)
{
for (int i = start; i <= end; i++)
{
for (int j = 0; j < lineExecState_.size(); j++)
{
int row = lineExecState_.get(j).getRow();
if (row == i)
{
if (state == ChunkRowExecState.LINE_QUEUED ||
state == ChunkRowExecState.LINE_NONE)
{
// we're cleaning up state, or queuing a line that still has
// state -- detach it immediately
lineExecState_.get(j).detach();
}
else
{
lineExecState_.get(j).setState(state);
}
break;
}
}
if (state == ChunkRowExecState.LINE_QUEUED)
{
// queued state: introduce to the editor
final Value<ChunkRowExecState> execState =
new Value<ChunkRowExecState>(null);
execState.setValue(
new ChunkRowExecState(editor_, i, state, new Command()
{
@Override
public void execute()
{
lineExecState_.remove(execState.getValue());
}
}));
lineExecState_.add(execState.getValue());
}
}
}
public boolean hasBreakpoints()
{
return breakpoints_.size() > 0;
}
private void updateBreakpoints(AceDocumentChangeEventNative changeEvent)
{
// if there are no breakpoints, don't do any work to move them about
if (breakpoints_.size() == 0)
{
return;
}
// see if we need to move any breakpoints around in response to
// this change to the document's text
String action = changeEvent.getAction();
Range range = changeEvent.getRange();
Position start = range.getStart();
Position end = range.getEnd();
// if the edit was all on one line or the action didn't change text
// in a way that could change lines, we can't have moved anything
if (start.getRow() == end.getRow() ||
(!action.equals("insertText") &&
!action.equals("insertLines") &&
!action.equals("removeText") &&
!action.equals("removeLines")))
{
return;
}
int shiftedBy = 0;
int shiftStartRow = 0;
// compute how many rows to shift
if (action == "insertText" ||
action == "insertLines")
{
shiftedBy = end.getRow() - start.getRow();
}
else
{
shiftedBy = start.getRow() - end.getRow();
}
// compute where to start shifting
shiftStartRow = start.getRow() +
((action == "insertText" && start.getColumn() > 0) ?
1 : 0);
// make a pass through the breakpoints and move them as appropriate:
// remove all the breakpoints after the row where the change
// happened, and add them back at their new position if they were
// not part of a deleted range.
ArrayList<Breakpoint> movedBreakpoints = new ArrayList<Breakpoint>();
for (int idx = 0; idx < breakpoints_.size(); idx++)
{
Breakpoint breakpoint = breakpoints_.get(idx);
int breakpointRow = rowFromLine(breakpoint.getEditorLineNumber());
if (breakpointRow >= shiftStartRow)
{
// remove the breakpoint from its old position
movedBreakpoints.add(breakpoint);
removeBreakpointMarker(breakpoint);
}
}
for (Breakpoint breakpoint: movedBreakpoints)
{
// calculate the new position of the breakpoint
int oldBreakpointPosition =
rowFromLine(breakpoint.getEditorLineNumber());
int newBreakpointPosition =
oldBreakpointPosition + shiftedBy;
// add a breakpoint in this new position only if it wasn't
// in a deleted range, and if we don't already have a
// breakpoint there
if (oldBreakpointPosition >= end.getRow() &&
!(oldBreakpointPosition == end.getRow() && shiftedBy < 0) &&
getBreakpointIdxByLine(lineFromRow(newBreakpointPosition)) < 0)
{
breakpoint.moveToLineNumber(lineFromRow(newBreakpointPosition));
placeBreakpointMarker(breakpoint);
fireEvent(new BreakpointMoveEvent(breakpoint.getBreakpointId()));
}
else
{
breakpoints_.remove(breakpoint);
fireEvent(new BreakpointSetEvent(
breakpoint.getEditorLineNumber(),
breakpoint.getBreakpointId(),
false));
}
}
}
private void placeBreakpointMarker(Breakpoint breakpoint)
{
int line = breakpoint.getEditorLineNumber();
if (breakpoint.getEditorState() == Breakpoint.STATE_ACTIVE)
{
editor_.getSession().setBreakpoint(rowFromLine(line));
}
else if (breakpoint.getEditorState() == Breakpoint.STATE_PROCESSING)
{
editor_.getRenderer().addGutterDecoration(
rowFromLine(line),
"ace_pending-breakpoint");
}
else if (breakpoint.getEditorState() == Breakpoint.STATE_INACTIVE)
{
editor_.getRenderer().addGutterDecoration(
rowFromLine(line),
"ace_inactive-breakpoint");
}
}
private void removeBreakpointMarker(Breakpoint breakpoint)
{
int line = breakpoint.getEditorLineNumber();
if (breakpoint.getEditorState() == Breakpoint.STATE_ACTIVE)
{
editor_.getSession().clearBreakpoint(rowFromLine(line));
}
else if (breakpoint.getEditorState() == Breakpoint.STATE_PROCESSING)
{
editor_.getRenderer().removeGutterDecoration(
rowFromLine(line),
"ace_pending-breakpoint");
}
else if (breakpoint.getEditorState() == Breakpoint.STATE_INACTIVE)
{
editor_.getRenderer().removeGutterDecoration(
rowFromLine(line),
"ace_inactive-breakpoint");
}
}
private void toggleBreakpointAtPosition(Position pos)
{
// rows are 0-based, but debug line numbers are 1-based
int lineNumber = lineFromRow(pos.getRow());
int breakpointIdx = getBreakpointIdxByLine(lineNumber);
// if there's already a breakpoint on that line, remove it
if (breakpointIdx >= 0)
{
Breakpoint breakpoint = breakpoints_.get(breakpointIdx);
removeBreakpointMarker(breakpoint);
fireEvent(new BreakpointSetEvent(
lineNumber,
breakpoint.getBreakpointId(),
false));
breakpoints_.remove(breakpointIdx);
}
// if there's no breakpoint on that line yet, create a new unset
// breakpoint there (the breakpoint manager will pick up the new
// breakpoint and attempt to set it on the server)
else
{
try
{
// move the breakpoint down to the first line that has a
// non-whitespace, non-comment token
if (editor_.getSession().getMode().getCodeModel() != null)
{
Position tokenPos = editor_.getSession().getMode().getCodeModel()
.findNextSignificantToken(pos);
if (tokenPos != null)
{
lineNumber = lineFromRow(tokenPos.getRow());
if (getBreakpointIdxByLine(lineNumber) >= 0)
{
return;
}
}
else
{
// if there are no tokens anywhere after the line, don't
// set a breakpoint
return;
}
}
}
catch (Exception e)
{
// If we failed at any point to fast-forward to the next line with
// a statement, we'll try to set a breakpoint on the line the user
// originally clicked.
}
fireEvent(new BreakpointSetEvent(
lineNumber,
BreakpointSetEvent.UNSET_BREAKPOINT_ID,
true));
}
}
private int getBreakpointIdxById(int breakpointId)
{
for (int idx = 0; idx < breakpoints_.size(); idx++)
{
if (breakpoints_.get(idx).getBreakpointId() == breakpointId)
{
return idx;
}
}
return -1;
}
private int getBreakpointIdxByLine(int lineNumber)
{
for (int idx = 0; idx < breakpoints_.size(); idx++)
{
if (breakpoints_.get(idx).getEditorLineNumber() == lineNumber)
{
return idx;
}
}
return -1;
}
private int lineFromRow(int row)
{
return row + 1;
}
private int rowFromLine(int line)
{
return line - 1;
}
// ---- Annotation related methods
private AnchoredRange createAnchoredRange(Position start,
Position end)
{
return getEditor().getSession().createAnchoredRange(start, end);
}
// This class binds an ace annotation (used for the gutter) with an
// inline marker (the underlining for associated lint). We also store
// the associated marker. Ie, with some beautiful ASCII art:
//
//
// 1. |
// 2. | foo <- function(apple) {
// /!\ | print(Apple)
// 3. | } ~~~~~
// 4. |
//
// The 'anchor' is associated with the position of the warning icon
// /!\; while the anchored range is associated with the underlying
// '~~~~~'. The marker id is needed to detach the annotation later.
private class AnchoredAceAnnotation
{
public AnchoredAceAnnotation(AceAnnotation annotation,
AnchoredRange range,
int markerId)
{
annotation_ = annotation;
range_ = range;
anchor_ = Anchor.createAnchor(
editor_.getSession().getDocument(),
annotation.row(),
annotation.column());
markerId_ = markerId;
}
public int getMarkerId() { return markerId_; }
public void detach()
{
if (range_ != null)
range_.detach();
if (anchor_ != null)
anchor_.detach();
editor_.getSession().removeMarker(markerId_);
}
public AceAnnotation asAceAnnotation()
{
return AceAnnotation.create(
anchor_.getRow(),
anchor_.getColumn(),
annotation_.text(),
annotation_.type());
}
private final AceAnnotation annotation_;
private final AnchoredRange range_;
private final Anchor anchor_;
private final int markerId_;
}
public JsArray<AceAnnotation> getAnnotations()
{
JsArray<AceAnnotation> annotations =
JsArray.createArray().cast();
annotations.setLength(annotations_.size());
for (int i = 0; i < annotations_.size(); i++)
annotations.set(i, annotations_.get(i).asAceAnnotation());
return annotations;
}
public void setAnnotations(JsArray<AceAnnotation> annotations)
{
clearAnnotations();
editor_.getSession().setAnnotations(annotations);
}
public void showLint(JsArray<LintItem> lint)
{
clearAnnotations();
JsArray<AceAnnotation> annotations = LintItem.asAceAnnotations(lint);
editor_.getSession().setAnnotations(annotations);
// Now, set (and cache) inline markers.
for (int i = 0; i < lint.length(); i++)
{
LintItem item = lint.get(i);
AnchoredRange range = createAnchoredRange(
Position.create(item.getStartRow(), item.getStartColumn()),
Position.create(item.getEndRow(), item.getEndColumn()));
String clazz = "unknown";
if (item.getType() == "error")
clazz = lintStyles_.error();
else if (item.getType() == "warning")
clazz = lintStyles_.warning();
else if (item.getType() == "info")
clazz = lintStyles_.info();
else if (item.getType() == "style")
clazz = lintStyles_.style();
int id = editor_.getSession().addMarker(range, clazz, "text", true);
annotations_.add(new AnchoredAceAnnotation(
annotations.get(i),
range,
id));
}
}
public void clearLint()
{
clearAnnotations();
editor_.getSession().setAnnotations(null);
}
private void updateAnnotations(AceDocumentChangeEventNative event)
{
Range range = event.getRange();
ArrayList<AnchoredAceAnnotation> annotations =
new ArrayList<AnchoredAceAnnotation>();
for (int i = 0; i < annotations_.size(); i++)
{
AnchoredAceAnnotation annotation = annotations_.get(i);
Position pos = annotation.anchor_.getPosition();
if (!range.contains(pos))
annotations.add(annotation);
else
annotation.detach();
}
annotations_ = annotations;
}
public void clearAnnotations()
{
for (int i = 0; i < annotations_.size(); i++)
annotations_.get(i).detach();
annotations_.clear();
}
public void removeMarkersOnCursorLine()
{
// Defer this so other event handling can update anchors etc.
Scheduler.get().scheduleDeferred(new ScheduledCommand()
{
@Override
public void execute()
{
int cursorRow = editor_.getCursorPosition().getRow();
JsArray<AceAnnotation> newAnnotations = JsArray.createArray().cast();
for (int i = 0; i < annotations_.size(); i++)
{
AnchoredAceAnnotation annotation = annotations_.get(i);
int markerId = annotation.getMarkerId();
Marker marker = editor_.getSession().getMarker(markerId);
// The marker may have already been removed in response to
// a previous action.
if (marker == null)
continue;
Range range = marker.getRange();
int rowStart = range.getStart().getRow();
int rowEnd = range.getEnd().getRow();
if (cursorRow >= rowStart && cursorRow <= rowEnd)
editor_.getSession().removeMarker(markerId);
else
newAnnotations.push(annotation.asAceAnnotation());
}
editor_.getSession().setAnnotations(newAnnotations);
editor_.getRenderer().renderMarkers();
}
});
}
public void removeMarkersAtCursorPosition()
{
// Defer this so other event handling can update anchors etc.
Scheduler.get().scheduleDeferred(new ScheduledCommand()
{
@Override
public void execute()
{
Position cursor = editor_.getCursorPosition();
JsArray<AceAnnotation> newAnnotations = JsArray.createArray().cast();
for (int i = 0; i < annotations_.size(); i++)
{
AnchoredAceAnnotation annotation = annotations_.get(i);
int markerId = annotation.getMarkerId();
Marker marker = editor_.getSession().getMarker(markerId);
// The marker may have already been removed in response to
// a previous action.
if (marker == null)
continue;
Range range = marker.getRange();
if (!range.contains(cursor))
newAnnotations.push(annotation.asAceAnnotation());
else
editor_.getSession().removeMarker(markerId);
}
editor_.getSession().setAnnotations(newAnnotations);
editor_.getRenderer().renderMarkers();
}
});
}
public void setDragEnabled(boolean enabled)
{
// the ACE API currently provides no way to disable dropping text
// from external sources specifically (the dragEnabled option affects
// only internal ACE dragging); for now, just put the whole editor into
// read-only mode while dragging, which prevents it from accepting the
// text
editor_.setReadOnly(!enabled);
}
public LineWidgetManager getLineWidgetManager()
{
return editor_.getLineWidgetManager();
}
public boolean isRendered()
{
return isRendered_;
}
private final AceEditorNative editor_;
private final HandlerManager capturingHandlers_;
private final List<HandlerRegistration> aceEventHandlers_;
private boolean initToEmptyString_ = true;
private boolean inOnChangeHandler_ = false;
private boolean isRendered_ = false;
private ArrayList<Breakpoint> breakpoints_ = new ArrayList<Breakpoint>();
private ArrayList<AnchoredAceAnnotation> annotations_ =
new ArrayList<AnchoredAceAnnotation>();
private ArrayList<ChunkRowExecState> lineExecState_ =
new ArrayList<ChunkRowExecState>();
private LintResources.Styles lintStyles_ = LintResources.INSTANCE.styles();
private EventBus events_;
private Commands commands_ = RStudioGinjector.INSTANCE.getCommands();
private static boolean hasEditHandlers_ = false;
}