blob: cdcff8849ec4a73961f9a340ae32ffca97b8e127 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2008, 2023 SAP AG and IBM Corporation.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* SAP AG - initial API and implementation
* Andrew Johnson - improved undo
*******************************************************************************/
package org.eclipse.mat.ui.internal.views;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.UncheckedIOException;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import java.util.zip.CRC32;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.Platform;
import org.eclipse.jface.action.Action;
import org.eclipse.jface.action.IMenuListener;
import org.eclipse.jface.action.IMenuManager;
import org.eclipse.jface.action.MenuManager;
import org.eclipse.jface.action.Separator;
import org.eclipse.jface.preference.JFacePreferences;
import org.eclipse.jface.resource.JFaceResources;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.Document;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.ITextListener;
import org.eclipse.jface.text.ITextOperationTarget;
import org.eclipse.jface.text.ITextViewer;
import org.eclipse.jface.text.Region;
import org.eclipse.jface.text.TextEvent;
import org.eclipse.jface.text.TextPresentation;
import org.eclipse.jface.text.TextViewer;
import org.eclipse.jface.text.TextViewerUndoManager;
import org.eclipse.jface.text.hyperlink.AbstractHyperlinkDetector;
import org.eclipse.jface.text.hyperlink.DefaultHyperlinkPresenter;
import org.eclipse.jface.text.hyperlink.IHyperlink;
import org.eclipse.jface.text.hyperlink.IHyperlinkDetector;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.mat.SnapshotException;
import org.eclipse.mat.query.ContextProvider;
import org.eclipse.mat.query.IContextObject;
import org.eclipse.mat.ui.editor.MultiPaneEditor;
import org.eclipse.mat.ui.util.PopupMenu;
import org.eclipse.mat.ui.util.QueryContextMenu;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.StyleRange;
import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.layout.FillLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.ui.IActionBars;
import org.eclipse.ui.IPartListener;
import org.eclipse.ui.ISaveablePart;
import org.eclipse.ui.ISaveablePart2;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.IWorkbenchPart;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.actions.ActionFactory;
import org.eclipse.ui.actions.ActionFactory.IWorkbenchAction;
import org.eclipse.ui.part.ViewPart;
public class NotesView extends ViewPart implements IPartListener, ISaveablePart, ISaveablePart2
{
private static final String NOTES_ENCODING = "UTF8"; //$NON-NLS-1$
private static final int UNDO_LEVEL = 25;
private File resource;
private Action undo;
private Action redo;
private Menu menu;
private Font font;
private Color hyperlinkColor;
private MultiPaneEditor editor;
private TextViewer textViewer;
private TextViewerUndoManager undoManager;
private Map<String, NotesViewAction> actions = new HashMap<String, NotesViewAction>();
long hash;
boolean modified;
@Override
public void createPartControl(Composite parent)
{
parent.setLayout(new FillLayout());
// No need for a dispose listener - the SaveablePart will save it
textViewer = new TextViewer(parent, SWT.MULTI | SWT.V_SCROLL | SWT.LEFT | SWT.H_SCROLL);
textViewer.setDocument(new Document());
textViewer.getControl().setEnabled(false);
textViewer.getTextWidget().setWordWrap(false);
font = JFaceResources.getFont("org.eclipse.mat.ui.notesfont"); //$NON-NLS-1$
textViewer.getControl().setFont(font);
hyperlinkColor = JFaceResources.getColorRegistry().get(JFacePreferences.HYPERLINK_COLOR);
getSite().getPage().addPartListener(this);
int undolevel = Platform.getPreferencesService().getInt("org.eclipse.ui.editors", "undoHistorySize", UNDO_LEVEL, null); //$NON-NLS-1$ //$NON-NLS-2$
undoManager = new TextViewerUndoManager(undolevel);
undoManager.connect(textViewer);
textViewer.setUndoManager(undoManager);
textViewer.addSelectionChangedListener(new ISelectionChangedListener()
{
public void selectionChanged(SelectionChangedEvent event)
{
updateActions();
}
});
textViewer.addTextListener(new ITextListener()
{
public void textChanged(TextEvent event)
{
modified = true;
searchForHyperlinks(textViewer.getDocument().get(), 0);
firePropertyChange(PROP_DIRTY);
}
});
textViewer.setHyperlinkPresenter(new DefaultHyperlinkPresenter(hyperlinkColor));
textViewer.setHyperlinkDetectors(new IHyperlinkDetector[] { new ObjectAddressHyperlinkDetector() }, SWT.MOD1);
makeActions();
hookContextMenu();
showBootstrapPart();
updateActions();
}
private void updateActions()
{
for (NotesViewAction a : actions.values())
a.setEnabled(textViewer.canDoOperation(a.actionId));
}
private void hookContextMenu()
{
MenuManager menuMgr = new MenuManager("#PopupMenu"); //$NON-NLS-1$
menuMgr.setRemoveAllWhenShown(true);
menuMgr.addMenuListener(new IMenuListener()
{
public void menuAboutToShow(IMenuManager manager)
{
textEditorContextMenuAboutToShow(manager);
}
});
Menu menu = menuMgr.createContextMenu(textViewer.getControl());
textViewer.getControl().setMenu(menu);
}
private void textEditorContextMenuAboutToShow(IMenuManager manager)
{
if (textViewer != null)
{
undo.setEnabled(undoManager.undoable());
redo.setEnabled(undoManager.redoable());
manager.add(undo);
manager.add(redo);
manager.add(new Separator());
manager.add(getAction(ActionFactory.CUT.getId()));
manager.add(getAction(ActionFactory.COPY.getId()));
manager.add(getAction(ActionFactory.PASTE.getId()));
manager.add(new Separator());
manager.add(getAction(ActionFactory.DELETE.getId()));
manager.add(getAction(ActionFactory.SELECT_ALL.getId()));
}
}
private NotesViewAction getAction(String actionID)
{
return actions.get(actionID);
}
private Action addAction(ActionFactory actionFactory, int textOperation, String actionDefinitionId)
{
IWorkbenchWindow window = getViewSite().getWorkbenchWindow();
IWorkbenchAction globalAction = actionFactory.create(window);
// Create our text action.
NotesViewAction action = new NotesViewAction(textOperation, actionDefinitionId);
actions.put(actionFactory.getId(), action);
// Copy its properties from the global action.
action.setText(globalAction.getText());
action.setToolTipText(globalAction.getToolTipText());
action.setDescription(globalAction.getDescription());
action.setImageDescriptor(globalAction.getImageDescriptor());
action.setDisabledImageDescriptor(globalAction.getDisabledImageDescriptor());
action.setAccelerator(globalAction.getAccelerator());
// Register our text action with the global action handler.
IActionBars actionBars = getViewSite().getActionBars();
actionBars.setGlobalActionHandler(actionFactory.getId(), action);
return action;
}
@Override
public void setFocus()
{
textViewer.getControl().setFocus();
}
public void partActivated(IWorkbenchPart part)
{
if (!supportsNotes(part))
return;
textViewer.getControl().setEnabled(true);
editor = (MultiPaneEditor) part;
File path = editor.getResourceFile();
if (path != null && !path.equals(resource))
{
if (isDirty())
saveNotes();
resource = path;
updateTextViewer();
}
}
public void partBroughtToTop(IWorkbenchPart part)
{
partActivated(part);
}
public void partClosed(IWorkbenchPart part)
{
if (!supportsNotes(part)) { return; }
MultiPaneEditor editor = (MultiPaneEditor) part;
File resource = editor.getResourceFile();
if (resource.equals(this.resource))
{
// Saving usually done as SaveablePart except when snapshot is closed
if (isDirty())
saveNotes();
this.resource = null;
this.editor = null;
this.updateTextViewer();
}
}
public void partDeactivated(IWorkbenchPart part)
{}
public void partOpened(IWorkbenchPart part)
{}
private void showBootstrapPart()
{
IWorkbenchPage page = getSite().getPage();
if (page != null)
partActivated(page.getActiveEditor());
}
private void makeActions()
{
// Install the standard text actions.
addAction(ActionFactory.CUT, ITextOperationTarget.CUT, "org.eclipse.ui.edit.cut");//$NON-NLS-1$
addAction(ActionFactory.COPY, ITextOperationTarget.COPY, "org.eclipse.ui.edit.copy");//$NON-NLS-1$
addAction(ActionFactory.PASTE, ITextOperationTarget.PASTE, "org.eclipse.ui.edit.paste");//$NON-NLS-1$
addAction(ActionFactory.DELETE, ITextOperationTarget.DELETE, "org.eclipse.ui.edit.delete");//$NON-NLS-1$
addAction(ActionFactory.SELECT_ALL, ITextOperationTarget.SELECT_ALL, "org.eclipse.ui.edit.selectAll");//$NON-NLS-1$
undo = addAction(ActionFactory.UNDO, ITextOperationTarget.UNDO, "org.eclipse.ui.edit.undo");//$NON-NLS-1$
redo = addAction(ActionFactory.REDO, ITextOperationTarget.REDO, "org.eclipse.ui.edit.redo");//$NON-NLS-1$
}
private long hash() {
// Used to detect if document has changed.
CRC32 crc = new CRC32();
try
{
crc.update(textViewer.getDocument().get().getBytes(NOTES_ENCODING));
}
catch (UnsupportedEncodingException e)
{
// Won't happen
return textViewer.getDocument().get().hashCode();
}
return crc.getValue();
}
private void updateTextViewer()
{
// get notes.txt and if it's not null set is as input
if (resource != null)
{
String buffer = readNotes(resource);
if (buffer != null)
{
Document document = new Document(buffer);
textViewer.setDocument(document);
revealEndOfDocument();
}
else
{
textViewer.setDocument(new Document(""));//$NON-NLS-1$
}
}
else
{
textViewer.setDocument(new Document(""));//$NON-NLS-1$
textViewer.getControl().setEnabled(false);
}
hash = hash();
modified = false;
firePropertyChange(PROP_DIRTY);
}
private void searchForHyperlinks(String allText, int offset)
{
if (resource == null)
return;
Pattern addressPattern = Pattern.compile("0x\\p{XDigit}+");//$NON-NLS-1$
String[] fields = allText.split("\\W", 0);//$NON-NLS-1$
List<IdHyperlink> hyperlinks = new ArrayList<IdHyperlink>();
for (String field : fields)
{
if (addressPattern.matcher(field).matches())
{
IRegion idRegion = new Region(offset, field.length());
IdHyperlink hyperlink = new IdHyperlink(field, editor, idRegion);
hyperlinks.add(hyperlink);
}
offset = offset + field.length() + 1; // length of the splitter
}
if (!hyperlinks.isEmpty())
highlightHyperlinks(hyperlinks);
}
private void highlightHyperlinks(List<IdHyperlink> hyperlinks)
{
TextPresentation style = new TextPresentation();
for (IHyperlink hyperlink : hyperlinks)
{
int startIndex = hyperlink.getHyperlinkRegion().getOffset();
int length = hyperlink.getHyperlinkRegion().getLength();
StyleRange styleRange = new StyleRange(startIndex, length, hyperlinkColor, null, SWT.ITALIC);
styleRange.underline = true;
style.addStyleRange(styleRange);
}
textViewer.changeTextPresentation(style, true);
}
private void saveNotes()
{
if (resource != null)
{
String text = textViewer.getDocument().get();
if (text != null)
saveNotes(resource, text);
resetUndoManager();
hash = hash();
modified = false;
}
}
@Override
public void dispose()
{
undoManager.disconnect();
getSite().getPage().removePartListener(this);
// The parent composite has been disposed, so there is no need to remove the disposeListener.
if (menu != null)
menu.dispose();
super.dispose();
}
private boolean supportsNotes(IWorkbenchPart part)
{
if (part instanceof MultiPaneEditor)
{
return true;
}
return false;
}
protected void revealEndOfDocument()
{
IDocument doc = textViewer.getDocument();
int docLength = doc.getLength();
if (docLength > 0)
{
textViewer.revealRange(docLength - 1, 1);
StyledText widget = textViewer.getTextWidget();
widget.setCaretOffset(docLength);
}
}
public void resetUndoManager()
{
undoManager.reset();
}
private final class IdHyperlink implements IHyperlink
{
String id;
MultiPaneEditor editor;
IRegion region;
public IdHyperlink(String id, MultiPaneEditor editor, IRegion region)
{
this.id = id;
this.editor = editor;
this.region = region;
}
public IRegion getHyperlinkRegion()
{
return region;
}
public String getHyperlinkText()
{
return null;
}
public String getTypeLabel()
{
return null;
}
public void open()
{
try
{
final int objectId = editor.getQueryContext().mapToObjectId(id);
if (objectId < 0)
return;
QueryContextMenu contextMenu = new QueryContextMenu(editor, new ContextProvider((String) null)
{
@Override
public IContextObject getContext(final Object row)
{
return new IContextObject()
{
public int getObjectId()
{
return (Integer) row;
}
};
}
});
PopupMenu popupMenu = new PopupMenu();
contextMenu.addContextActions(popupMenu, new StructuredSelection(objectId), null);
if (menu != null && !menu.isDisposed())
menu.dispose();
menu = popupMenu.createMenu(getViewSite().getActionBars().getStatusLineManager(), PlatformUI
.getWorkbench().getDisplay().getActiveShell());
menu.setVisible(true);
}
catch (NumberFormatException ignore)
{
// $JL-EXC$
// shouldn't happen and if it does nothing can be done
}
catch (SnapshotException ignore)
{
// $JL-EXC$
// mapToAddress throws exception on illegal values
}
}
}
private class ObjectAddressHyperlinkDetector extends AbstractHyperlinkDetector
{
public IHyperlink[] detectHyperlinks(ITextViewer textViewer, IRegion region, boolean canShowMultipleHyperlinks)
{
if (region == null || textViewer == null)
return null;
IDocument document = textViewer.getDocument();
int offset = region.getOffset();
if (document == null)
return null;
IRegion lineInfo;
String text;
try
{
lineInfo = document.getLineInformationOfOffset(offset);
text = document.get(lineInfo.getOffset(), lineInfo.getLength());
}
catch (BadLocationException ex)
{
return null;
}
int index = offset - lineInfo.getOffset();
// to the left from offset
char ch;
do
{
index--;
ch = ' ';
if (index > -1)
ch = text.charAt(index);
}
while (Character.isLetterOrDigit(ch));
int startIndex = index + 1;
// to the right from offset
index = offset - lineInfo.getOffset() - 1;
do
{
index++;
if (index >= text.length())
{
break;
}
ch = text.charAt(index);
}
while (Character.isLetterOrDigit(ch));
Pattern addressPattern = Pattern.compile("0x\\p{XDigit}+");//$NON-NLS-1$
String address = text.substring(startIndex, index);
if (address != null && addressPattern.matcher(address).matches())
{
IRegion idRegion = new Region(startIndex, address.length());
return new IHyperlink[] { new IdHyperlink(address, editor, idRegion) };
}
else
return null;
}
}
private class NotesViewAction extends Action
{
private int actionId;
NotesViewAction(int actionId, String actionDefinitionId)
{
this.actionId = actionId;
this.setActionDefinitionId(actionDefinitionId);
}
@Override
public boolean isEnabled()
{
return textViewer.canDoOperation(actionId);
}
public void run()
{
textViewer.doOperation(actionId);
}
}
// //////////////////////////////////////////////////////////////
// notes management
// //////////////////////////////////////////////////////////////
/**
* Read the contents of the notes file, based on the
* snapshot resource.
* @param resourcePath The editor file (snapshot or index file).
* @return The contents of the notes file, lines separated by \n.
* @throws {@link UncheckedIOException} for some IO errors.
*/
public static String readNotes(File resourcePath)
{
if (resourcePath != null)
{
File notesFile = getDefaultNotesFile(resourcePath);
if (notesFile.canRead() && notesFile.isFile())
{
try (FileInputStream fileInput = new FileInputStream(notesFile))
{
BufferedReader myInput = new BufferedReader(new InputStreamReader(fileInput, NOTES_ENCODING));
try
{
String s;
StringBuffer b = new StringBuffer();
while ((s = myInput.readLine()) != null)
{
b.append(s);
b.append("\n");//$NON-NLS-1$
}
return b.toString();
}
finally
{
try
{
myInput.close();
}
catch (IOException ignore)
{}
}
}
catch (FileNotFoundException e)
{
throw new UncheckedIOException(notesFile.getPath(), e);
}
catch (IOException e)
{
throw new UncheckedIOException(notesFile.getPath(), e);
}
}
}
return null;
}
private static void saveNotes(File resource, String notes)
{
OutputStream fout = null;
try
{
File notesFile = getDefaultNotesFile(resource);
fout = new FileOutputStream(notesFile);
OutputStreamWriter out = new OutputStreamWriter(new BufferedOutputStream(fout), NOTES_ENCODING);
try
{
out.write(notes);
out.flush();
}
finally
{
out.close();
}
}
catch (FileNotFoundException e)
{
throw new RuntimeException(e);
}
catch (IOException e)
{
throw new RuntimeException(e);
}
finally
{
try
{
if (fout != null)
fout.close();
}
catch (IOException ignore)
{}
}
}
private static File getDefaultNotesFile(File resource)
{
String filename = resource.getName();
int p = filename.lastIndexOf('.');
if (p >= 0)
filename = filename.substring(0, p);
return new File(resource.getParentFile(), filename + ".notes.txt");//$NON-NLS-1$
}
public void doSave(IProgressMonitor monitor)
{
saveNotes();
firePropertyChange(PROP_DIRTY);
}
public void doSaveAs()
{
}
public boolean isDirty()
{
return undoManager.undoable() || modified && hash != hash();
}
public boolean isSaveAsAllowed()
{
return false;
}
public boolean isSaveOnCloseNeeded()
{
return true;
}
public int promptToSaveOnClose()
{
return ISaveablePart2.YES;
}
}