package com.redhat.ceylon.eclipse.code.editor; /******************************************************************************* * Copyright (c) 2007 IBM Corporation. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Robert Fuhrer (rfuhrer@watson.ibm.com) - initial API and implementation *******************************************************************************/ import static com.redhat.ceylon.compiler.typechecker.parser.CeylonLexer.ASTRING_LITERAL; import static com.redhat.ceylon.compiler.typechecker.parser.CeylonLexer.AVERBATIM_STRING; import static com.redhat.ceylon.compiler.typechecker.parser.CeylonLexer.STRING_END; import static com.redhat.ceylon.compiler.typechecker.parser.CeylonLexer.STRING_LITERAL; import static com.redhat.ceylon.compiler.typechecker.parser.CeylonLexer.STRING_MID; import static com.redhat.ceylon.compiler.typechecker.parser.CeylonLexer.VERBATIM_STRING; import static com.redhat.ceylon.eclipse.code.outline.HierarchyView.showHierarchyView; import static com.redhat.ceylon.eclipse.code.preferences.CeylonPreferenceInitializer.PASTE_CORRECT_INDENTATION; import static com.redhat.ceylon.eclipse.code.preferences.CeylonPreferenceInitializer.PASTE_ESCAPE_QUOTED; import static com.redhat.ceylon.eclipse.code.preferences.CeylonPreferenceInitializer.PASTE_IMPORTS; import static com.redhat.ceylon.eclipse.util.Nodes.getTokenStrictlyContainingOffset; import static java.lang.Character.isWhitespace; import static org.eclipse.jface.text.DocumentRewriteSessionType.SEQUENTIAL; import java.util.Map; import org.antlr.runtime.ANTLRStringStream; import org.antlr.runtime.CommonToken; import org.antlr.runtime.CommonTokenStream; import org.eclipse.jface.preference.IPreferenceStore; import org.eclipse.jface.text.AbstractInformationControlManager; import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.DocumentCommand; import org.eclipse.jface.text.DocumentRewriteSession; import org.eclipse.jface.text.IAutoEditStrategy; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.IDocumentExtension4; import org.eclipse.jface.text.IRegion; import org.eclipse.jface.text.information.IInformationPresenter; import org.eclipse.jface.text.presentation.IPresentationReconciler; import org.eclipse.jface.text.source.IOverviewRuler; import org.eclipse.jface.text.source.IVerticalRuler; import org.eclipse.jface.text.source.SourceViewerConfiguration; import org.eclipse.jface.text.source.projection.ProjectionViewer; import org.eclipse.swt.SWTError; import org.eclipse.swt.dnd.Clipboard; import org.eclipse.swt.dnd.DND; import org.eclipse.swt.dnd.RTFTransfer; import org.eclipse.swt.dnd.TextTransfer; import org.eclipse.swt.dnd.Transfer; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Display; import org.eclipse.text.edits.MultiTextEdit; import org.eclipse.text.edits.ReplaceEdit; import org.eclipse.ui.PartInitException; import com.redhat.ceylon.compiler.typechecker.parser.CeylonLexer; import com.redhat.ceylon.compiler.typechecker.tree.Tree; import com.redhat.ceylon.compiler.typechecker.util.NewlineFixingStringStream; import com.redhat.ceylon.eclipse.code.correct.correctJ2C; import com.redhat.ceylon.eclipse.code.parse.CeylonParseController; import com.redhat.ceylon.eclipse.ui.CeylonPlugin; import com.redhat.ceylon.ide.common.imports.SelectedImportsVisitor; import com.redhat.ceylon.model.typechecker.model.Declaration; public class CeylonSourceViewer extends ProjectionViewer { /** * Text operation code for requesting the outline for * the current input. */ public static final int SHOW_OUTLINE= 51; /** * Text operation code for requesting the outline for * the element at the current position. */ public static final int OPEN_STRUCTURE= 52; /** * Text operation code for requesting the hierarchy for * the current input. */ public static final int SHOW_HIERARCHY= 53; /** * Text operation code for requesting the code for the * current input. */ public static final int SHOW_DEFINITION= 56; /** * Text operation code for requesting the references for * the current input. */ public static final int SHOW_REFERENCES= 59; /** * Text operation code for toggling the commenting of a * selected range of text, or the current line. */ public static final int TOGGLE_COMMENT= 54; public static final int ADD_BLOCK_COMMENT= 57; public static final int REMOVE_BLOCK_COMMENT= 58; /** * Text operation code for toggling the display of * "occurrences" of the current selection. */ public static final int MARK_OCCURRENCES= 55; /** * Text operation code for correcting the indentation of * the currently selected text. */ public static final int CORRECT_INDENTATION= 60; /** * Text operation code for requesting the hierarchy for * the current input. */ public static final int SHOW_IN_HIERARCHY_VIEW= 70; private IInformationPresenter outlinePresenter; private IInformationPresenter structurePresenter; private IInformationPresenter hierarchyPresenter; private IInformationPresenter definitionPresenter; private IInformationPresenter referencesPresenter; private IAutoEditStrategy autoEditStrategy; private CeylonEditor editor; public CeylonSourceViewer(CeylonEditor ceylonEditor, Composite parent, IVerticalRuler verticalRuler, IOverviewRuler overviewRuler, boolean showAnnotationsOverview, int styles) { super(parent, verticalRuler, overviewRuler, showAnnotationsOverview, styles); this.editor = ceylonEditor; } public CeylonSourceViewer(Composite parent, IVerticalRuler verticalRuler, IOverviewRuler overviewRuler, boolean showAnnotationsOverview, int styles) { this(null, parent, verticalRuler, overviewRuler, showAnnotationsOverview, styles); } public boolean canDoOperation(int operation) { switch(operation) { case SHOW_OUTLINE: return outlinePresenter!=null; case OPEN_STRUCTURE: return structurePresenter!=null; case SHOW_HIERARCHY: return hierarchyPresenter!=null; case SHOW_DEFINITION: return definitionPresenter!=null; case SHOW_REFERENCES: return referencesPresenter!=null; case SHOW_IN_HIERARCHY_VIEW: return true; case ADD_BLOCK_COMMENT: //TODO: check if something is selected! case REMOVE_BLOCK_COMMENT: //TODO: check if there is a block comment in the selection! case TOGGLE_COMMENT: return true; case CORRECT_INDENTATION: return autoEditStrategy!=null; } return super.canDoOperation(operation); } public void doOperation(int operation) { try { if (getTextWidget() == null) { super.doOperation(operation); return; } String selectedText = editor.getSelectionText(); Map<Declaration,String> imports = null; switch (operation) { case SHOW_OUTLINE: if (outlinePresenter!=null) outlinePresenter.showInformation(); return; case OPEN_STRUCTURE: if (structurePresenter!=null) structurePresenter.showInformation(); return; case SHOW_HIERARCHY: if (hierarchyPresenter!=null) hierarchyPresenter.showInformation(); return; case SHOW_DEFINITION: if (definitionPresenter!=null) definitionPresenter.showInformation(); return; case SHOW_REFERENCES: if (referencesPresenter!=null) referencesPresenter.showInformation(); return; case SHOW_IN_HIERARCHY_VIEW: showHierarchy(); return; case TOGGLE_COMMENT: doToggleComment(); return; case ADD_BLOCK_COMMENT: addBlockComment(); return; case REMOVE_BLOCK_COMMENT: removeBlockComment(); return; case CORRECT_INDENTATION: Point selectedRange = getSelectedRange(); doCorrectIndentation(selectedRange.x, selectedRange.y); return; case PASTE: if (localPaste()) return; break; case CUT: case COPY: imports = copyImports(); break; } super.doOperation(operation); switch (operation) { case CUT: case COPY: afterCopyCut(selectedText, imports); break; /*case PASTE: afterPaste(textWidget); break;*/ } } catch (Exception e) { //never propagate exceptions e.printStackTrace(); } } public void showHierarchy() throws PartInitException { showHierarchyView().focusOnSelection(editor); } private void afterCopyCut(String selectedText, Map<Declaration,String> imports) { if (imports!=null && !editor.isBlockSelectionModeEnabled()) { IRegion selection = editor.getSelection(); int offset = selection.getOffset(); IDocument doc = this.getDocument(); CommonToken token = getContainingToken(offset, doc); boolean quoted; if (token == null) { quoted = false; } else { int tt = token.getType(); //don't include verbatim strings! quoted = tt==ASTRING_LITERAL || tt==STRING_LITERAL || tt==STRING_END || tt==STRING_END || tt==STRING_MID; } char c = quoted ? '"' : '{'; Display display = getTextWidget().getDisplay(); Clipboard clipboard = new Clipboard(display); try { Object text = clipboard.getContents( TextTransfer.getInstance()); Object rtf = clipboard.getContents( RTFTransfer.getInstance()); try { Object[] data; Transfer[] dataTypes; if (rtf==null) { data = new Object[] { text, imports, c + selectedText }; dataTypes = new Transfer[] { TextTransfer.getInstance(), ImportsTransfer.INSTANCE, SourceTransfer.INSTANCE }; } else { data = new Object[] { text, rtf, imports, c + selectedText }; dataTypes = new Transfer[] { TextTransfer.getInstance(), RTFTransfer.getInstance(), ImportsTransfer.INSTANCE, SourceTransfer.INSTANCE }; } clipboard.setContents(data, dataTypes); } catch (SWTError e) { if (e.code != DND.ERROR_CANNOT_SET_CLIPBOARD) { throw e; } e.printStackTrace(); } } finally { clipboard.dispose(); } } } private boolean localPaste() { if (!editor.isBlockSelectionModeEnabled()) { Clipboard clipboard = new Clipboard(getTextWidget().getDisplay()); try { String text = (String) clipboard.getContents( SourceTransfer.INSTANCE); boolean fromStringLiteral; if (text==null) { fromStringLiteral = false; text = (String) clipboard.getContents( TextTransfer.getInstance()); } else { fromStringLiteral = text.charAt(0)=='"'; text = text.substring(1); } if (text==null) { return false; } else { @SuppressWarnings({"unchecked", "rawtypes"}) Map<Declaration,String> imports = (Map) clipboard.getContents( ImportsTransfer.INSTANCE); IRegion selection = editor.getSelection(); int offset = selection.getOffset(); int length = selection.getLength(); int endOffset = offset+length; IDocument doc = this.getDocument(); DocumentRewriteSession rewriteSession = null; if (doc instanceof IDocumentExtension4) { rewriteSession = ((IDocumentExtension4) doc) .startRewriteSession(SEQUENTIAL); } CommonToken token = getContainingToken(offset, doc); boolean quoted; boolean verbatim; // int startOfTokenInLine; if (token == null) { quoted = false; verbatim = false; // startOfTokenInLine = -1; } else { int tt = token.getType(); quoted = tt==ASTRING_LITERAL || tt==STRING_LITERAL || tt==STRING_END || tt==STRING_END || tt==STRING_MID || tt==VERBATIM_STRING || tt==AVERBATIM_STRING; verbatim = tt==VERBATIM_STRING || tt==AVERBATIM_STRING; // startOfTokenInLine = // token.getCharPositionInLine() + // (verbatim ? 3 : 1); } try { boolean startOfLine = isStartOfLine(offset, doc); IPreferenceStore prefs = CeylonPlugin.getPreferences(); try { MultiTextEdit edit = new MultiTextEdit(); if (!quoted && imports!=null && prefs.getBoolean(PASTE_IMPORTS)) { pasteImports(imports, edit, text, doc); } if (quoted && !verbatim && !fromStringLiteral && prefs.getBoolean(PASTE_ESCAPE_QUOTED)) { text = text .replace("\\", "\\\\") .replace("\t", "\\t") .replace("\"", "\\\"") .replace("`", "\\`"); } if ((!quoted || verbatim) && fromStringLiteral && prefs.getBoolean(PASTE_ESCAPE_QUOTED)) { text = text .replace("\\\"", "\"") .replace("\\`", "`") .replace("\\t", "\t") .replace("\\\\", "\\"); } edit.addChild(new ReplaceEdit(offset, length, text)); edit.apply(doc); IRegion region = edit.getRegion(); endOffset = region.getOffset() + region.getLength(); } catch (Exception e) { e.printStackTrace(); return false; } try { if (startOfLine && prefs.getBoolean(PASTE_CORRECT_INDENTATION)) { endOffset = correctSourceIndentation( endOffset-text.length(), text.length(), doc) + 1; } return true; } catch (Exception e) { e.printStackTrace(); return true; } } finally { if (doc instanceof IDocumentExtension4) { ((IDocumentExtension4) doc) .stopRewriteSession(rewriteSession); } setSelectedRange(endOffset, 0); } } } finally { clipboard.dispose(); } } else { return false; } } public CommonToken getContainingToken(int offset, IDocument doc) { ANTLRStringStream stream = new NewlineFixingStringStream(doc.get()); CeylonLexer lexer = new CeylonLexer(stream); CommonTokenStream tokens = new CommonTokenStream(lexer); tokens.fill(); return getTokenStrictlyContainingOffset(offset, tokens.getTokens()); } private static boolean isStartOfLine( int offset, IDocument doc) { try { int lineStart = doc.getLineInformationOfOffset(offset) .getOffset(); int positionInLine = offset-lineStart; return doc.get(lineStart, positionInLine) .trim().isEmpty(); } catch (BadLocationException e) { e.printStackTrace(); return false; } } private void addBlockComment() { IDocument doc = this.getDocument(); DocumentRewriteSession rewriteSession = null; Point p = this.getSelectedRange(); if (doc instanceof IDocumentExtension4) { rewriteSession = ((IDocumentExtension4) doc) .startRewriteSession(SEQUENTIAL); } try { final int selStart = p.x; final int selLen = p.y; final int selEnd = selStart+selLen; doc.replace(selStart, 0, "/*"); doc.replace(selEnd+2, 0, "*/"); } catch (BadLocationException e) { e.printStackTrace(); } finally { if (doc instanceof IDocumentExtension4) { ((IDocumentExtension4) doc) .stopRewriteSession(rewriteSession); } restoreSelection(); } } private void removeBlockComment() { IDocument doc = this.getDocument(); DocumentRewriteSession rewriteSession = null; Point p = this.getSelectedRange(); if (doc instanceof IDocumentExtension4) { rewriteSession = ((IDocumentExtension4) doc) .startRewriteSession(SEQUENTIAL); } try { final int selStart = p.x; final int selLen = p.y; final int selEnd = selStart+selLen; String text = doc.get(); int open = text.indexOf("/*", selStart); if (open>selEnd) open = -1; if (open<0) { open = text.lastIndexOf("/*", selStart); } int close = -1; if (open>=0) { close = text.indexOf("*/", open); } if (close+2<selStart) close = -1; if (open>=0&&close>=0) { doc.replace(open, 2, ""); doc.replace(close-2, 2, ""); } } catch (BadLocationException e) { e.printStackTrace(); } finally { if (doc instanceof IDocumentExtension4) { ((IDocumentExtension4) doc) .stopRewriteSession(rewriteSession); } restoreSelection(); } } private void doToggleComment() { IDocument doc = this.getDocument(); DocumentRewriteSession rewriteSession = null; Point p = this.getSelectedRange(); final String lineCommentPrefix = "//"; if (doc instanceof IDocumentExtension4) { rewriteSession = ((IDocumentExtension4) doc) .startRewriteSession(SEQUENTIAL); } try { final int selStart = p.x; final int selLen = p.y; final int selEnd = selStart+selLen; final int startLine = doc.getLineOfOffset(selStart); int endLine = doc.getLineOfOffset(selEnd); if (selLen>0 && lookingAtLineEnd(doc, selEnd)) endLine--; boolean linesAllHaveCommentPrefix = linesHaveCommentPrefix(doc, lineCommentPrefix, startLine, endLine); boolean useCommonLeadingSpace = true; // take from a preference? int leadingSpaceToUse = useCommonLeadingSpace ? calculateLeadingSpace(doc, startLine, endLine) : 0; for (int line = startLine; line<=endLine; line++) { int lineStart = doc.getLineOffset(line); int lineEnd = lineStart+doc.getLineLength(line)-1; if (linesAllHaveCommentPrefix) { // remove the comment prefix from each line, wherever it occurs in the line int offset = lineStart; while (isWhitespace(doc.getChar(offset)) && offset<lineEnd) { offset++; } // The first non-whitespace characters *must* be the single-line comment prefix doc.replace(offset, lineCommentPrefix.length(), ""); } else { // add the comment prefix to each line, after however many spaces leadingSpaceToAdd indicates int offset = lineStart+leadingSpaceToUse; doc.replace(offset, 0, lineCommentPrefix); } } } catch (BadLocationException e) { e.printStackTrace(); } finally { if (doc instanceof IDocumentExtension4) { ((IDocumentExtension4) doc) .stopRewriteSession(rewriteSession); } restoreSelection(); } } private int calculateLeadingSpace(IDocument doc, int startLine, int endLine) { try { int result = Integer.MAX_VALUE; for (int line=startLine; line<=endLine; line++) { int lineStart = doc.getLineOffset(line); int lineEnd = lineStart + doc.getLineLength(line) - 1; int offset = lineStart; while (isWhitespace(doc.getChar(offset)) && offset < lineEnd) { offset++; } int leadingSpaces = offset - lineStart; result = Math.min(result, leadingSpaces); } return result; } catch (BadLocationException e) { return 0; } } /** * @return true, if the given inclusive range of lines * all start with the single-line comment prefix, even * if they have different amounts of leading whitespace */ private boolean linesHaveCommentPrefix(IDocument doc, String lineCommentPrefix, int startLine, int endLine) { try { int docLen = doc.getLength(); for (int line=startLine; line<=endLine; line++) { int lineStart = doc.getLineOffset(line); int lineEnd = lineStart + doc.getLineLength(line) - 1; int offset = lineStart; while (isWhitespace(doc.getChar(offset)) && offset < lineEnd) { offset++; } if (docLen-offset > lineCommentPrefix.length() && doc.get(offset, lineCommentPrefix.length()) .equals(lineCommentPrefix)) { // this line starts with the single-line comment prefix } else { return false; } } } catch (BadLocationException e) { return false; } return true; } private void doCorrectIndentation(int offset, int len) { IDocument doc = getDocument(); DocumentRewriteSession rewriteSession = null; if (doc instanceof IDocumentExtension4) { rewriteSession = ((IDocumentExtension4) doc) .startRewriteSession(SEQUENTIAL); } Point selectedRange = getSelectedRange(); boolean emptySelection = selectedRange==null || selectedRange.y==0; try { correctSourceIndentation(offset, len, doc); } catch (BadLocationException e) { e.printStackTrace(); } finally { if (doc instanceof IDocumentExtension4) { ((IDocumentExtension4) doc) .stopRewriteSession(rewriteSession); } restoreSelection(); if (emptySelection) { selectedRange = getSelectedRange(); setSelectedRange(selectedRange.x, 0); } } } public int correctSourceIndentation(int selStart, int selLen, IDocument doc) throws BadLocationException { int selEnd = selStart + selLen; int startLine = doc.getLineOfOffset(selStart); int endLine = doc.getLineOfOffset(selEnd); // If the selection extends just to the beginning of the next line, don't indent that one too if (selLen > 0 && lookingAtLineEnd(doc, selEnd)) { endLine--; } int endOffset = selStart+selLen-1; // Indent each line using the AutoEditStrategy for (int line=startLine; line<=endLine; line++) { int lineStartOffset = doc.getLineOffset(line); // Replace the existing indentation with the desired indentation. // Use the language-specific AutoEditStrategy, which requires a DocumentCommand. DocumentCommand cmd = new DocumentCommand() { }; cmd.offset = lineStartOffset; cmd.length = 0; cmd.text = Character.toString('\t'); cmd.doit = true; cmd.shiftsCaret = false; autoEditStrategy.customizeDocumentCommand(doc, cmd); if (cmd.text!=null) { doc.replace(cmd.offset, cmd.length, cmd.text); endOffset += cmd.text.length()-cmd.length; } } return endOffset; } private boolean lookingAtLineEnd(IDocument doc, int pos) { String[] legalLineTerms = doc.getLegalLineDelimiters(); try { for(String lineTerm: legalLineTerms) { int len = lineTerm.length(); if (pos>len && doc.get(pos-len,len).equals(lineTerm)) { return true; } } } catch (BadLocationException e) { e.printStackTrace(); } return false; } public void configure(SourceViewerConfiguration configuration) { super.configure(configuration); AbstractInformationControlManager hoverController = getTextHoveringController(); if (hoverController!=null) { //null in a merge viewer hoverController.setSizeConstraints(80, 30, false, true); } if (configuration instanceof CeylonSourceViewerConfiguration) { CeylonSourceViewerConfiguration svc = (CeylonSourceViewerConfiguration) configuration; outlinePresenter = svc.getOutlinePresenter(this); if (outlinePresenter!=null) { outlinePresenter.install(this); } structurePresenter = svc.getOutlinePresenter(this); if (structurePresenter!=null) { structurePresenter.install(this); } hierarchyPresenter = svc.getHierarchyPresenter(this); if (hierarchyPresenter!=null) { hierarchyPresenter.install(this); } definitionPresenter = svc.getDefinitionPresenter(this); if (definitionPresenter!=null) { definitionPresenter.install(this); } referencesPresenter = svc.getReferencesPresenter(this); if (referencesPresenter!=null) { referencesPresenter.install(this); } autoEditStrategy = new CeylonAutoEditStrategy(); } } public void unconfigure() { if (outlinePresenter != null) { outlinePresenter.uninstall(); outlinePresenter= null; } if (structurePresenter != null) { structurePresenter.uninstall(); structurePresenter= null; } if (hierarchyPresenter != null) { hierarchyPresenter.uninstall(); hierarchyPresenter= null; } super.unconfigure(); } Map<Declaration,String> copyImports() { try { CeylonParseController controller = editor.getParseController(); if (controller==null) { return null; } Tree.CompilationUnit cu = controller.getTypecheckedRootNode(); if (cu == null) { return null; } IRegion selection = editor.getSelection(); SelectedImportsVisitor v = new SelectedImportsVisitor( selection.getOffset(), selection.getLength()); cu.visit(v); return v.getCopiedReferencesMap(); } catch (Exception e) { e.printStackTrace(); return null; } } void pasteImports(Map<Declaration,String> map, MultiTextEdit edit, String pastedText, IDocument doc) { if (!map.isEmpty()) { CeylonParseController controller = editor.getParseController(); if (controller==null || controller.getLastCompilationUnit()==null) { return; } new correctJ2C().pasteImports(map, edit, doc, controller.getLastCompilationUnit()); } } public IPresentationReconciler getPresentationReconciler() { return fPresentationReconciler; } public CeylonContentAssistant getContentAssistant() { return (CeylonContentAssistant) fContentAssistant; } }