/*
* Copyright 2013-2016 consulo.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package consulo.web.gwt.client.ui;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.Style;
import com.google.gwt.event.dom.client.KeyCodes;
import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.ui.*;
import consulo.web.gwt.client.service.EditorColorSchemeService;
import consulo.web.gwt.client.util.GwtStyleUtil;
import consulo.web.gwt.client.util.GwtUIUtil;
import consulo.web.gwt.client.util.GwtUtil;
import consulo.web.gwt.client.util.ReportableCallable;
import consulo.web.gwt.shared.transport.*;
import org.jetbrains.annotations.Nullable;
import java.util.Arrays;
import java.util.List;
/**
* @author VISTALL
* @since 17-May-16
*/
public class Editor extends SimplePanel implements WidgetWithUpdateUI {
private static class CodeLinePanel extends FlowPanel implements WidgetWithUpdateUI {
private Editor myEditor;
private int myLine;
public CodeLinePanel(Editor editor, int line) {
myEditor = editor;
myLine = line;
sinkEvents(Event.ONCLICK);
}
@Override
public void onBrowserEvent(Event event) {
switch (DOM.eventGetType(event)) {
case Event.ONCLICK:
myEditor.changeLine(this);
break;
default:
event.preventDefault();
break;
}
}
@Override
public void updateUI() {
Element parentElement = getElement().getParentElement();
GwtEditorColorScheme scheme = myEditor.getScheme();
if (myEditor.myCurrentLinePanel == this) {
// we need change color td element, due we have padding
parentElement.getStyle().setBackgroundColor(GwtStyleUtil.toString(scheme.getColor(GwtEditorColorScheme.CARET_ROW_COLOR)));
}
else {
myEditor.setDefaultTextColors(parentElement);
}
}
public int getLine() {
return myLine;
}
}
public static class LineNumberSpan extends InlineHTML implements WidgetWithUpdateUI {
private Editor myEditor;
public LineNumberSpan(String html, Editor editor) {
super(html);
myEditor = editor;
updateUI();
}
@Override
public void updateUI() {
GwtEditorColorScheme scheme = myEditor.getScheme();
getElement().getStyle().setColor(GwtStyleUtil.toString(scheme.getColor(GwtEditorColorScheme.LINE_NUMBERS_COLOR)));
}
}
public static class MainGrid extends Grid implements WidgetWithUpdateUI {
private Editor myEditor;
public MainGrid(Editor editor, int rows, int columns) {
super(rows, columns);
myEditor = editor;
updateUI();
}
@Override
public void updateUI() {
myEditor.setDefaultTextColors(this);
}
}
public static class GutterPanel extends Grid implements WidgetWithUpdateUI {
private Editor myEditor;
public GutterPanel(int lineCount, Editor editor) {
super(lineCount + 1, 1);
myEditor = editor;
// dummy element - fill free space with same background and resize it
set(lineCount, new InlineHTML(""));
getCellFormatter().getElement(lineCount, 0).getStyle().setHeight(100, Style.Unit.PCT);
updateUI();
}
public void set(int row, Widget widget) {
setWidget(row, 0, widget);
}
@Override
public void updateUI() {
GwtEditorColorScheme scheme = myEditor.getScheme();
getElement().getStyle().setProperty("borderRightColor", GwtStyleUtil.toString(scheme.getColor(GwtEditorColorScheme.TEARLINE_COLOR)));
getElement().getStyle().setProperty("borderRightStyle", "solid");
getElement().getStyle().setProperty("borderRightWidth", "1px");
getElement().getStyle().setWhiteSpace(Style.WhiteSpace.NOWRAP);
getElement().getStyle().setBackgroundColor(GwtStyleUtil.toString(scheme.getColor(GwtEditorColorScheme.GUTTER_BACKGROUND)));
}
}
public static class GutterLineGrid extends Grid implements WidgetWithUpdateUI {
private Editor myEditor;
private int myLine;
public GutterLineGrid(int rows, int columns, Editor editor, int line) {
super(rows, columns);
myEditor = editor;
myLine = line;
}
@Override
public void updateUI() {
final Style style = getElement().getStyle();
GwtEditorColorScheme scheme = myEditor.getScheme();
if (myEditor.myCurrentLinePanel != null && myEditor.myCurrentLinePanel.myLine == myLine) {
style.setBackgroundColor(GwtStyleUtil.toString(scheme.getColor(GwtEditorColorScheme.CARET_ROW_COLOR)));
}
else {
style.setBackgroundColor(GwtStyleUtil.toString(scheme.getColor(GwtEditorColorScheme.GUTTER_BACKGROUND)));
}
}
}
public static final int ourLexerFlag = 1 << 1;
public static final int ourEditorFlag = 1 << 2;
public static final int ourSelectFlag = 1 << 24;
@Nullable
private EditorSegmentBuilder myBuilder;
private int myLineCount;
private int myDelayedCaredOffset = -1;
private int myLastCaretOffset = -1;
private final EditorTabPanel myEditorTabPanel;
private final String myFileUrl;
private GwtTextRange myLastCursorPsiElementTextRange;
private GwtNavigateInfo myLastNavigationInfo;
private CodeLinePanel myCurrentLinePanel;
private GwtEditorColorScheme myScheme;
private GutterPanel myGutterPanel;
private boolean myInsideGutter;
private DecoratedPopupPanel myLastTooltip;
private GwtTextRange myLastTooltipRange;
enum HighlightState {
UNKNOWN,
LEXER,
PASS
}
private HighlightState myHighlightState = HighlightState.UNKNOWN;
private EditorColorSchemeService.Listener myListener = new EditorColorSchemeService.Listener() {
@Override
public void schemeChanged(GwtEditorColorScheme scheme) {
myScheme = scheme;
Scheduler.get().scheduleDeferred(new Command() {
@Override
public void execute() {
GwtUIUtil.updateUI(Editor.this);
doHighlightImpl();
}
});
}
};
public Editor(final EditorTabPanel editorTabPanel, String fileUrl, final String text) {
myEditorTabPanel = editorTabPanel;
myFileUrl = fileUrl;
sinkEvents(Event.ONCLICK | Event.MOUSEEVENTS | Event.ONKEYUP);
final EditorColorSchemeService schemeService = GwtUtil.get(EditorColorSchemeService.KEY);
schemeService.addListener(myListener);
myScheme = schemeService.getScheme();
setDefaultTextColors(this);
addStyleName("scroll");
GwtUIUtil.fill(this);
setWidget(GwtUIUtil.loadingPanel());
Scheduler.get().scheduleDeferred(new Command() {
@Override
public void execute() {
myBuilder = new EditorSegmentBuilder(text);
myLineCount = myBuilder.getLineCount();
setWidget(build());
if (myDelayedCaredOffset != -1) {
focusOffset(myDelayedCaredOffset);
myDelayedCaredOffset = -1;
}
doHighlightImpl();
}
});
}
private void doHighlightImpl() {
GwtUtil.rpc().getLexerHighlight(myFileUrl, new ReportableCallable<List<GwtHighlightInfo>>() {
@Override
public void onSuccess(List<GwtHighlightInfo> result) {
addHighlightInfos(result, Editor.ourLexerFlag);
myHighlightState = HighlightState.LEXER;
runHighlightPasses(myLastCaretOffset, null);
myHighlightState = HighlightState.PASS;
}
});
}
private void runHighlightPasses(int offset, final Runnable callback) {
GwtUtil.rpc().runHighlightPasses(myFileUrl, offset, new ReportableCallable<List<GwtHighlightInfo>>() {
@Override
public void onSuccess(List<GwtHighlightInfo> result) {
addHighlightInfos(result, Editor.ourEditorFlag);
if (callback != null) {
callback.run();
}
}
});
}
@Override
public void updateUI() {
// update main panel
setDefaultTextColors(this);
// update current line
if (myCurrentLinePanel != null) {
myCurrentLinePanel.updateUI();
}
}
private void setDefaultTextColors(Widget widget) {
setDefaultTextColors(widget.getElement());
}
private void setDefaultTextColors(Element element) {
GwtTextAttributes textAttr = myScheme.getAttributes(GwtEditorColorScheme.TEXT);
if (textAttr != null) {
GwtColor background = textAttr.getBackground();
if (background != null) {
element.getStyle().setBackgroundColor(GwtStyleUtil.toString(background));
}
else {
element.getStyle().clearBackgroundColor();
}
GwtColor foreground = textAttr.getForeground();
if (foreground != null) {
element.getStyle().setColor(GwtStyleUtil.toString(foreground));
}
else {
element.getStyle().clearColor();
}
}
}
public void dispose() {
final EditorColorSchemeService schemeService = GwtUtil.get(EditorColorSchemeService.KEY);
schemeService.removeListener(myListener);
}
private boolean insideGutter(Element element) {
Element temp = element;
while (temp != null) {
// if we entered editor element
if (temp == getElement()) {
break;
}
if (myGutterPanel.getElement() == temp) {
getElement().getStyle().setCursor(Style.Cursor.DEFAULT);
return true;
}
temp = temp.getParentElement();
}
return false;
}
@Override
public void onBrowserEvent(final Event event) {
if (myBuilder == null) {
return;
}
switch (DOM.eventGetType(event)) {
case Event.ONMOUSEOVER: {
com.google.gwt.dom.client.Element element = DOM.eventGetToElement(event);
myInsideGutter = insideGutter(element);
if (myInsideGutter) {
return;
}
Object range = element == null ? null : element.getPropertyObject("range");
if (!(range instanceof GwtTextRange)) {
return;
}
final int startOffset = ((GwtTextRange)range).getStartOffset();
final Widget widget = (Widget)element.getPropertyObject("widget");
if (event.getCtrlKey()) {
if (myHighlightState == HighlightState.UNKNOWN) {
return;
}
GwtUtil.rpc().getNavigationInfo(myFileUrl, startOffset, new ReportableCallable<GwtNavigateInfo>() {
@Override
public void onSuccess(GwtNavigateInfo result) {
if (result == null) {
return;
}
GwtTextRange resultElementRange = result.getRange();
if (myLastCursorPsiElementTextRange != null && myLastCursorPsiElementTextRange.containsRange(resultElementRange)) {
return;
}
getElement().getStyle().setCursor(Style.Cursor.POINTER);
if (result.getDocText() != null) {
myLastTooltipRange = myLastCursorPsiElementTextRange;
showTooltip(widget, result.getDocText());
}
else {
removeTooltip();
}
myLastCursorPsiElementTextRange = resultElementRange;
GwtHighlightInfo highlightInfo =
new GwtHighlightInfo(myScheme.getAttributes(GwtEditorColorScheme.CTRL_CLICKABLE), resultElementRange, Integer.MAX_VALUE);
myLastNavigationInfo = result;
addHighlightInfos(Arrays.asList(highlightInfo), ourSelectFlag);
}
});
}
else {
if (widget instanceof EditorSegmentBuilder.CharSpan) {
myLastTooltipRange = ((EditorSegmentBuilder.CharSpan)widget).range;
String toolTip = ((EditorSegmentBuilder.CharSpan)widget).getToolTip();
if (toolTip != null) {
showTooltip(widget, toolTip);
}
}
else {
myLastTooltipRange = null;
removeTooltip();
}
}
event.preventDefault();
break;
}
case Event.ONKEYUP:
if (event.getKeyCode() == KeyCodes.KEY_CTRL) {
removeTooltip();
}
break;
case Event.ONMOUSEOUT: {
boolean old = myInsideGutter;
myInsideGutter = insideGutter(DOM.eventGetToElement(event));
if (old != myInsideGutter) {
getElement().getStyle().clearCursor();
}
if (myInsideGutter) {
event.preventDefault();
return;
}
com.google.gwt.dom.client.Element element = DOM.eventGetToElement(event);
GwtTextRange range = element == null ? null : (GwtTextRange)element.getPropertyObject("range");
if (range == null || myLastTooltipRange != null && !myLastTooltipRange.containsRange(range)) {
removeTooltip();
}
onMouseOut();
event.preventDefault();
break;
}
case Event.ONCLICK: {
if (myInsideGutter) {
event.preventDefault();
return;
}
com.google.gwt.dom.client.Element element = DOM.eventGetTarget(event);
int offset = 0;
Object spanRange = element.getPropertyObject("range");
if (spanRange != null) {
offset = ((GwtTextRange)spanRange).getStartOffset();
}
else {
Object lineRange = element.getPropertyObject("lineRange");
if (lineRange != null) {
offset = ((GwtTextRange)lineRange).getStartOffset();
}
}
if (offset == myLastCaretOffset) {
return;
}
myLastCaretOffset = offset;
if (event.getCtrlKey()) {
if (myLastNavigationInfo != null) {
List<GwtNavigatable> navigates = myLastNavigationInfo.getNavigates();
GwtNavigatable navigatable = navigates.get(0);
onMouseOut();
myEditorTabPanel.openFileInEditor(navigatable.getFile(), navigatable.getOffset());
}
}
else {
onOffsetChangeImpl();
}
event.preventDefault();
break;
}
default:
super.onBrowserEvent(event);
break;
}
}
private void onOffsetChangeImpl() {
if (myLastCaretOffset == -1 || myHighlightState != HighlightState.PASS) {
return;
}
runHighlightPasses(myLastCaretOffset, null);
}
private void onMouseOut() {
if (myBuilder == null) {
return;
}
if (myLastCursorPsiElementTextRange != null) {
getElement().getStyle().clearCursor();
myBuilder.removeHighlightByRange(myLastCursorPsiElementTextRange, ourSelectFlag);
myLastCursorPsiElementTextRange = null;
myLastNavigationInfo = null;
}
}
private Widget build() {
Grid gridPanel = GwtUIUtil.fillAndReturn(new MainGrid(this, 1, 2));
// try to fill area by code
gridPanel.getColumnFormatter().getElement(1).getStyle().setWidth(100, Style.Unit.PCT);
gridPanel.getRowFormatter().setVerticalAlign(0, HasVerticalAlignment.ALIGN_TOP);
myGutterPanel = GwtUIUtil.fillAndReturn(new GutterPanel(myLineCount, this));
gridPanel.setWidget(0, 0, myGutterPanel);
myGutterPanel.addStyleName("noselectable");
for (int i = 0; i < myLineCount; i++) {
final GutterLineGrid panel = GwtUIUtil.fillAndReturn(new GutterLineGrid(1, 2, this, i));
panel.updateUI();
// place lines to right
panel.getCellFormatter().setHorizontalAlignment(0, 0, HasHorizontalAlignment.ALIGN_RIGHT);
panel.getCellFormatter().getElement(0, 0).getStyle().setPaddingLeft(5, Style.Unit.PX);
panel.getCellFormatter().getElement(0, 1).getStyle().setPaddingRight(5, Style.Unit.PX);
panel.getCellFormatter().getElement(0, 1).addClassName("editorLine");
// try fill line number as primary panel
panel.getColumnFormatter().getElement(0).getStyle().setWidth(100, Style.Unit.PCT);
LineNumberSpan lineSpan = new LineNumberSpan(String.valueOf(i + 1), this);
lineSpan.addStyleName("editorLine");
panel.setWidget(0, 0, lineSpan);
myGutterPanel.set(i, panel);
}
Grid editorCodePanel = new Grid(myLineCount + 1, 1) {
{
sinkEvents(Event.ONCHANGE | Event.ONPASTE | Event.KEYEVENTS);
}
@Override
public void onBrowserEvent(Event event) {
int type = DOM.eventGetType(event);
switch (type) {
case Event.ONKEYDOWN:
switch (event.getKeyCode()) {
case KeyCodes.KEY_B:
if (event.getCtrlKey()) {
GwtUtil.rpc().getNavigationInfo(myFileUrl, myLastCaretOffset, new ReportableCallable<GwtNavigateInfo>() {
@Override
public void onSuccess(GwtNavigateInfo result) {
if (result == null) {
return;
}
GwtNavigatable navigatable = result.getNavigates().get(0);
myEditorTabPanel.openFileInEditor(navigatable.getFile(), navigatable.getOffset());
}
});
}
break;
}
break;
}
event.preventDefault();
}
};
gridPanel.setWidget(0, 1, editorCodePanel);
GwtUIUtil.fill(editorCodePanel);
// dont provide red code
editorCodePanel.getElement().setAttribute("spellcheck", "false");
// editable
editorCodePanel.getElement().setAttribute("contenteditable", "true");
// disable border
editorCodePanel.addStyleName("noFocusBorder");
// inside one row - with fully fill
editorCodePanel.setWidget(myLineCount, 0, new InlineHTML(""));
editorCodePanel.getCellFormatter().getElement(myLineCount, 0).getStyle().setHeight(100, Style.Unit.PCT);
int lineCount = 0;
CodeLinePanel lineElement = null;
int startOffset = 0;
for (EditorSegmentBuilder.CharSpan fragment : myBuilder.getFragments()) {
if (lineElement == null) {
lineElement = new CodeLinePanel(this, lineCount);
setDefaultTextColors(lineElement);
lineElement.setWidth("100%");
lineElement.addStyleName("editorLine");
startOffset = fragment.range.getStartOffset();
}
lineElement.add(fragment);
if (fragment.lineWrap) {
editorCodePanel.getCellFormatter().getElement(lineCount, 0).getStyle().setPaddingLeft(5, Style.Unit.PX);
editorCodePanel.setWidget(lineCount, 0, lineElement);
lineElement.getElement().setPropertyObject("lineRange", new GwtTextRange(startOffset, fragment.range.getEndOffset()));
lineElement.updateUI(); // update after adding
lineElement = null;
lineCount++;
}
}
return gridPanel;
}
public GwtEditorColorScheme getScheme() {
return myScheme;
}
private void removeTooltip() {
myLastTooltipRange = null;
if (myLastTooltip != null) {
myLastTooltip.hide();
myLastTooltip = null;
}
}
private void showTooltip(Widget widget, String html) {
removeTooltip();
myLastTooltip = new DecoratedPopupPanel(true);
myLastTooltip.setWidget(new HTML(html));
int left = widget.getAbsoluteLeft();
int top = widget.getAbsoluteTop() + 16;
myLastTooltip.setPopupPosition(left, top);
myLastTooltip.show();
}
public void addHighlightInfos(List<GwtHighlightInfo> result, int flag) {
if (myBuilder == null) {
return;
}
myBuilder.addHighlights(result, flag);
}
public void setCaretOffset(int offset) {
myLastCaretOffset = offset;
focusOffset(offset);
onOffsetChangeImpl();
}
public void focusOffset(int offset) {
if (myBuilder == null) {
myDelayedCaredOffset = offset;
return;
}
myLastCaretOffset = offset;
for (EditorSegmentBuilder.CharSpan fragment : myBuilder.getFragments()) {
if (fragment.range.containsRange(offset, offset)) {
fragment.getElement().focus();
fragment.getElement().scrollIntoView();
set(fragment.getElement());
Widget parent = fragment.getParent();
if (parent instanceof CodeLinePanel) {
changeLine((CodeLinePanel)parent);
}
break;
}
}
}
public void changeLine(CodeLinePanel widget) {
if (myCurrentLinePanel == widget) {
return;
}
CodeLinePanel currentLinePanel = myCurrentLinePanel;
if (currentLinePanel != null) {
myCurrentLinePanel = null; // drop current line
currentLinePanel.updateUI();
GwtUIUtil.updateUI(myGutterPanel);
}
myCurrentLinePanel = widget;
myCurrentLinePanel.updateUI();
GwtUIUtil.updateUI(myGutterPanel);
}
public native void set(Element element) /*-{
var range = $doc.createRange();
var sel = $wnd.getSelection();
range.setStart(element, 1);
range.setEnd(element, 1);
range.collapse(true);
sel.removeAllRanges();
sel.addRange(range);
element.focus();
}-*/;
}