blob: 9c87adae133f2e660b61920c7efade9a74de50f4 [file] [log] [blame]
/*=============================================================================#
# Copyright (c) 2009, 2020 Stephan Wahlbrink and others.
#
# This program and the accompanying materials are made available under the
# terms of the Eclipse Public License 2.0 which is available at
# https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
# which is available at https://www.apache.org/licenses/LICENSE-2.0.
#
# SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
#
# Contributors:
# Stephan Wahlbrink <sw@wahlbrink.eu> - initial API and implementation
#=============================================================================*/
package org.eclipse.statet.internal.r.ui.rhelp;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Collections;
import java.util.List;
import org.eclipse.core.commands.AbstractHandler;
import org.eclipse.core.commands.ExecutionEvent;
import org.eclipse.core.commands.ExecutionException;
import org.eclipse.core.commands.IHandler2;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.core.runtime.preferences.IEclipsePreferences.IPreferenceChangeListener;
import org.eclipse.core.runtime.preferences.IEclipsePreferences.PreferenceChangeEvent;
import org.eclipse.jface.action.IMenuListener;
import org.eclipse.jface.action.IMenuManager;
import org.eclipse.jface.action.IStatusLineManager;
import org.eclipse.jface.action.IToolBarManager;
import org.eclipse.jface.text.ITextSelection;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.ui.IActionBars;
import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.IPartListener2;
import org.eclipse.ui.IWorkbenchPart;
import org.eclipse.ui.IWorkbenchPartReference;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.browser.IWebBrowser;
import org.eclipse.ui.handlers.IHandlerService;
import org.eclipse.ui.menus.CommandContributionItem;
import org.eclipse.ui.menus.CommandContributionItemParameter;
import org.eclipse.ui.part.IPage;
import org.eclipse.ui.part.IShowInTarget;
import org.eclipse.ui.part.ShowInContext;
import org.eclipse.ui.progress.IProgressService;
import org.eclipse.ui.services.IServiceLocator;
import org.eclipse.ui.statushandlers.StatusManager;
import org.eclipse.statet.jcommons.lang.NonNull;
import org.eclipse.statet.jcommons.lang.NonNullByDefault;
import org.eclipse.statet.jcommons.lang.Nullable;
import org.eclipse.statet.jcommons.status.ProgressMonitor;
import org.eclipse.statet.jcommons.status.StatusException;
import org.eclipse.statet.jcommons.status.eplatform.EStatusUtils;
import org.eclipse.statet.ecommons.preferences.core.util.PreferenceUtils;
import org.eclipse.statet.ecommons.text.TextUtil;
import org.eclipse.statet.ecommons.ui.SharedUIResources;
import org.eclipse.statet.ecommons.ui.actions.HandlerCollection;
import org.eclipse.statet.ecommons.ui.actions.HandlerContributionItem;
import org.eclipse.statet.ecommons.ui.actions.SimpleContributionItem;
import org.eclipse.statet.ecommons.ui.actions.ToggleBooleanPreferenceHandler;
import org.eclipse.statet.ecommons.ui.actions.UIActions;
import org.eclipse.statet.ecommons.ui.mpbv.BookmarkCollection;
import org.eclipse.statet.ecommons.ui.mpbv.BrowserBookmark;
import org.eclipse.statet.ecommons.ui.mpbv.BrowserHandler;
import org.eclipse.statet.ecommons.ui.mpbv.BrowserSession;
import org.eclipse.statet.ecommons.ui.mpbv.PageBookBrowserPage;
import org.eclipse.statet.ecommons.ui.mpbv.PageBookBrowserView;
import org.eclipse.statet.ecommons.ui.util.UIAccess;
import org.eclipse.statet.ecommons.ui.workbench.ContextHandlers;
import org.eclipse.statet.internal.r.debug.ui.RLaunchingMessages;
import org.eclipse.statet.internal.r.ui.rhelp.RequestSync.RequestKey;
import org.eclipse.statet.ltk.ast.core.util.AstSelection;
import org.eclipse.statet.ltk.ui.ISelectionWithElementInfoListener;
import org.eclipse.statet.ltk.ui.LTKInputData;
import org.eclipse.statet.ltk.ui.sourceediting.SourceEditor1;
import org.eclipse.statet.ltk.ui.util.LTKSelectionUtils;
import org.eclipse.statet.r.core.RCore;
import org.eclipse.statet.r.core.model.RElementName;
import org.eclipse.statet.r.core.model.RModel;
import org.eclipse.statet.r.core.model.RSourceUnit;
import org.eclipse.statet.r.core.rsource.ast.RAstNode;
import org.eclipse.statet.r.launching.RCodeLaunching;
import org.eclipse.statet.r.ui.RUI;
import org.eclipse.statet.rhelp.core.REnvHelp;
import org.eclipse.statet.rhelp.core.RHelpPage;
import org.eclipse.statet.rhelp.core.RPkgHelp;
import org.eclipse.statet.rhelp.core.http.RHelpHttpService;
import org.eclipse.statet.rj.renv.core.REnv;
@NonNullByDefault
public class RHelpView extends PageBookBrowserView
implements ISelectionWithElementInfoListener, IShowInTarget {
public class RunCode extends AbstractHandler {
private final boolean gotoConsole;
public RunCode(final boolean gotoConsole) {
this.gotoConsole= gotoConsole;
}
@Override
public void setEnabled(final @Nullable Object evaluationContext) {
setBaseEnabled(getCurrentBrowserPage() != null);
}
@Override
public @Nullable Object execute(final ExecutionEvent event) throws ExecutionException {
final PageBookBrowserPage browserPage= getCurrentBrowserPage();
if (browserPage != null) {
final String selectedText= browserPage.getSelectedText();
if (selectedText != null && selectedText.length() > 0) {
try {
final List<String> lines= TextUtil.toLines(selectedText);
RCodeLaunching.runRCodeDirect(lines, this.gotoConsole, null);
}
catch (final CoreException e) {
final IStatus causeStatus= e.getStatus();
final Status status= new Status(causeStatus.getSeverity(), RUI.BUNDLE_ID, 0,
RLaunchingMessages.RSelectionLaunch_error_message, e );
StatusManager.getManager().handle(status);
final IStatusLineManager manager= getViewSite().getActionBars().getStatusLineManager();
if (manager != null) {
if (causeStatus.getSeverity() == IStatus.ERROR) {
manager.setErrorMessage(causeStatus.getMessage());
}
else {
manager.setMessage(causeStatus.getMessage());
}
}
}
}
}
return null;
}
}
private class LinkEditorHandler extends SimpleContributionItem {
public LinkEditorHandler() {
super("Link with Editor", null,
SharedUIResources.getImages().getDescriptor(SharedUIResources.LOCTOOL_SYNCHRONIZED_IMAGE_ID), null,
STYLE_CHECK );
}
@Override
protected void execute() throws ExecutionException {
setLinkingWithEditor(!RHelpView.this.isLinkingWithEditor);
}
}
private @Nullable IPreferenceChangeListener prefListener;
private boolean isLinkingWithEditor;
private final LinkEditorHandler linkingWithEditorHandler= new LinkEditorHandler();
private @Nullable SourceEditor1 linkedEditor;
private @Nullable IPartListener2 partListener;
private final RequestSync requestSync= new RequestSync();
public RHelpView() {
super();
}
@Override
public void dispose() {
{ final IPartListener2 partListener= this.partListener;
if (partListener != null) {
this.partListener= null;
getSite().getPage().removePartListener(partListener);
}
final SourceEditor1 linkedEditor= this.linkedEditor;
if (linkedEditor != null) {
this.linkedEditor= null;
linkedEditor.removePostSelectionWithElementInfoListener(this);
}
}
{ final IPreferenceChangeListener prefListener= this.prefListener;
if (prefListener != null) {
this.prefListener= null;
PreferenceUtils.getInstancePrefs().removePreferenceNodeListener(
RHelpPreferences.RHELP_QUALIFIER, prefListener );
}
}
super.dispose();
}
@Override
public void createPartControl(final Composite parent) {
super.createPartControl(parent);
{ final IPreferenceChangeListener prefListener= new IPreferenceChangeListener() {
@Override
public void preferenceChange(final PreferenceChangeEvent event) {
if (event.getKey().equals(RHelpPreferences.SHOW_INTERNAL_ENABLED_KEY)) {
UIAccess.getDisplay().asyncExec(RHelpView.this::reloadRPkgHelps);
}
}
};
this.prefListener= prefListener;
PreferenceUtils.getInstancePrefs().addPreferenceNodeListener(
RHelpPreferences.RHELP_QUALIFIER, prefListener );
}
if (!PlatformUI.getWorkbench().isStarting()) {
final Job job= new Job("Initial R Help Page") { //$NON-NLS-1$
{ setSystem(true);
setUser(false);
setPriority(Job.SHORT);
}
@Override
protected IStatus run(final IProgressMonitor monitor) {
RCore.getRHelpHttpService().ensureIsRunning();
final Display display= UIAccess.getDisplay();
if (getCurrentBrowserPage() == null
&& display != null && !display.isDisposed()) {
display.asyncExec(new Runnable() {
@Override
public void run() {
if (getCurrentBrowserPage() == null
&& UIAccess.isOkToUse(getPageBook())) {
newPage(null, true);
}
}
});
}
return Status.OK_STATUS;
}
};
job.schedule(50);
}
initLinking();
}
private void initLinking() {
this.partListener= new IPartListener2() {
@Override
public void partOpened(final IWorkbenchPartReference partRef) {
}
@Override
public void partClosed(final IWorkbenchPartReference partRef) {
if (RHelpView.this.linkedEditor != null && partRef.getPart(false) == RHelpView.this.linkedEditor) {
clear();
}
}
@Override
public void partVisible(final IWorkbenchPartReference partRef) {
}
@Override
public void partHidden(final IWorkbenchPartReference partRef) {
}
@Override
public void partInputChanged(final IWorkbenchPartReference partRef) {
}
@Override
public void partActivated(final IWorkbenchPartReference partRef) {
final IWorkbenchPart part= partRef.getPart(false);
if (part instanceof SourceEditor1) {
final SourceEditor1 sourceEditor= (SourceEditor1)part;
RHelpView.this.linkedEditor= sourceEditor;
sourceEditor.addPostSelectionWithElementInfoListener(RHelpView.this);
}
else if (part instanceof IEditorPart) {
clear();
}
}
@Override
public void partDeactivated(final IWorkbenchPartReference partRef) {
}
@Override
public void partBroughtToTop(final IWorkbenchPartReference partRef) {
}
private void clear() {
final SourceEditor1 sourceEditor= RHelpView.this.linkedEditor;
if (sourceEditor != null) {
sourceEditor.removePostSelectionWithElementInfoListener(RHelpView.this);
RHelpView.this.linkedEditor= null;
}
}
};
getSite().getPage().addPartListener(this.partListener);
}
@Override
protected void initActions(final IServiceLocator serviceLocator, final ContextHandlers handlers) {
super.initActions(serviceLocator, handlers);
final IHandlerService handlerService= serviceLocator.getService(IHandlerService.class);
{ final IHandler2 handler= new RunCode(false);
handlers.add(RCodeLaunching.SUBMIT_SELECTION_COMMAND_ID, handler);
handlerService.activateHandler(RCodeLaunching.SUBMIT_SELECTION_COMMAND_ID, handler);
}
{ final IHandler2 handler= new RunCode(true);
handlers.add(RCodeLaunching.SUBMIT_FILEVIACOMMAND_GOTOCONSOLE_COMMAND_ID, handler);
handlerService.activateHandler(RCodeLaunching.SUBMIT_FILEVIACOMMAND_GOTOCONSOLE_COMMAND_ID, handler);
}
}
@Override
protected @NonNull IHandler2 createOpenExternalHandler() {
return new BrowserHandler.OpenExternalHandler(getBrowserInterface()) {
@Override
protected void open(final String url, final IWebBrowser webBrowser) throws Exception {
try {
final URI serverUrl= RCore.getRHelpHttpService().toServerUrl(new URI(url));
if (serverUrl != null) {
webBrowser.openURL(serverUrl.toURL());
return;
}
}
catch (final URISyntaxException | MalformedURLException e) {}
super.open(url, webBrowser);
}
};
}
@Override
protected void contributeToActionBars(final IServiceLocator serviceLocator,
final IActionBars actionBars, final HandlerCollection handlers) {
super.contributeToActionBars(serviceLocator, actionBars, handlers);
final IMenuManager menuManager= actionBars.getMenuManager();
menuManager.appendToGroup(UIActions.VIEW_FILTER_GROUP_ID, new HandlerContributionItem(
new CommandContributionItemParameter(serviceLocator,
"Show.Internal", HandlerContributionItem.NO_COMMAND_ID, null,
null, null, null,
"Show &Internal Topics", null, null,
HandlerContributionItem.STYLE_RADIO, null, false),
new ToggleBooleanPreferenceHandler(RHelpPreferences.SHOW_INTERNAL_ENABLED_PREF, null) ));
menuManager.addMenuListener(new IMenuListener() {
@Override
public void menuAboutToShow(final IMenuManager manager) {
manager.find("Show.Internal").update();
}
});
final IToolBarManager toolBarManager= actionBars.getToolBarManager();
toolBarManager.appendToGroup("bookmarks", //$NON-NLS-1$
new CommandContributionItem(new CommandContributionItemParameter(
serviceLocator, null, "org.eclipse.statet.workbench.commands.OpenSearchDialog", //$NON-NLS-1$
Collections.singletonMap("pageId", "org.eclipse.statet.r.searchPages.RHelpPage"), //$NON-NLS-1$ //$NON-NLS-2$
null, null, null,
null, null, null,
CommandContributionItem.STYLE_PUSH, null, false)));
toolBarManager.add(this.linkingWithEditorHandler);
}
@Override
protected PageBookBrowserPage doCreatePage(final BrowserSession session) {
return new RHelpViewPage(this, session);
}
@Override
protected void updateTitle() {
final BrowserSession session= getCurrentSession();
if (session == null) {
setContentDescription(getNoPageTitle());
}
else {
setContentDescription(""); //$NON-NLS-1$
}
}
@Override
protected BookmarkCollection initBookmarkCollection() {
final BookmarkCollection collection= BookmarkCollection.getCollection(RHelpPreferences.RHELP_QUALIFIER);
final List<BrowserBookmark> bookmarks= collection.getBookmarks();
synchronized (collection) {
if (bookmarks.isEmpty()) {
bookmarks.add(new BrowserBookmark("R Homepage - The R Project for Statistical Computing", "https://www.r-project.org/")); //$NON-NLS-1$ //$NON-NLS-2$
bookmarks.add(new BrowserBookmark("CRAN - The Comprehensive R Archive Network", "https://cran.r-project.org/")); //$NON-NLS-1$ //$NON-NLS-2$
bookmarks.add(new BrowserBookmark("RSeek.org - R community Search Engine", "https://rseek.org/")); //$NON-NLS-1$ //$NON-NLS-2$
}
}
return collection;
}
@Override
protected @Nullable BrowserBookmark createBookmark() {
final PageBookBrowserPage page= getCurrentBrowserPage();
if (page != null) {
String sUrl= page.getCurrentUrl();
try {
final URI url= new URI(sUrl);
final URI portableUrl= RCore.getRHelpHttpService().toPortableUrl(url);
sUrl= (portableUrl != null) ? portableUrl.toString() : url.toString();
}
catch (final URISyntaxException e) {
// ?
}
return new BrowserBookmark(page.getCurrentTitle(), sUrl);
}
return null;
}
@Override
public String getHomePageUrl() {
return PreferenceUtils.getInstancePrefs().getPreferenceValue(RHelpPreferences.HOMEPAGE_URL_PREF);
}
@Override
protected void collectContextMenuPreferencePages(final List<String> pageIds) {
pageIds.add("org.eclipse.statet.r.preferencePages.RHelpPage"); //$NON-NLS-1$
pageIds.add("org.eclipse.statet.r.preferencePages.REnvironmentPage"); //$NON-NLS-1$
super.collectContextMenuPreferencePages(pageIds);
}
private void reloadRPkgHelps() {
for (final BrowserSession browserSession : getSessions()) {
final IPage page= getPage(browserSession);
if (page instanceof RHelpViewPage) {
final RHelpViewPage helpViewPage= (RHelpViewPage)page;
if (helpViewPage.getHelpObject() instanceof RPkgHelp) {
helpViewPage.refresh();
}
}
}
}
public void setLinkingWithEditor(final boolean enable) {
this.isLinkingWithEditor= enable;
this.linkingWithEditorHandler.setChecked(enable);
if (enable && this.linkedEditor != null) {
final ISelection selection= this.linkedEditor.getShowInContext().getSelection();
if (selection instanceof LTKInputData) {
stateChanged((LTKInputData) selection);
}
}
}
@Override
public void inputChanged() {
}
@Override
public void stateChanged(final LTKInputData state) {
if (!this.isLinkingWithEditor) {
return;
}
show(state, this.requestSync.newRequest(false));
}
private boolean show(final LTKInputData state, final @Nullable RequestKey request) {
if (request == null) {
return false;
}
try {
if (state.getInputElement().getModelTypeId() == RModel.R_TYPE_ID
|| state.getInputElement() instanceof RSourceUnit) {
final AstSelection astSelection= state.getAstSelection();
final RSourceUnit rSourceUnit= (RSourceUnit)state.getInputElement();
final ISelection selection= state.getSelection();
if (astSelection != null && selection instanceof ITextSelection) {
final ITextSelection textSelection= (ITextSelection) selection;
if (!(astSelection.getCovering() instanceof RAstNode) || textSelection.getLength() > 0) {
return false;
}
final RAstNode rNode= (RAstNode) astSelection.getCovering();
RElementName name= null;
if (!rNode.hasChildren()) {
name= RHelpLtkUI.searchName(rNode, rNode, false);
}
if (name == null) {
name= RHelpLtkUI.searchNameOfFunction(rNode,
LTKSelectionUtils.toTextRegion(textSelection) );
}
if (name == null) {
return false;
}
if (this.requestSync.startRequest(request)) {
doShow1(request, rSourceUnit, rNode, name);
}
}
}
return false;
}
finally {
if (request.job == null) {
this.requestSync.deleteRequest(request);
}
}
}
private boolean doShow1(final RequestKey request,
final @Nullable RSourceUnit rSourceUnit, final RAstNode rNode, final RElementName name) {
final REnvHelp help;
Object helpObject= null;
try {
help= RHelpLtkUI.getEnvHelp(rSourceUnit);
}
catch (final StatusException e) {
final IStatus status= EStatusUtils.convert(e.getStatus());
showMessage(status, request);
return false;
}
try {
if (RElementName.isPackageFacetScopeType(name.getType())) {
helpObject= help.getPkgHelp(name.getSegmentName());
}
else {
if (name.getScope() != null
&& RElementName.isPackageFacetScopeType(name.getScope().getType()) ) {
final RPkgHelp pkgHelp= help.getPkgHelp(name.getScope().getSegmentName());
if (pkgHelp != null) {
helpObject= pkgHelp.getPageForTopic(name.getSegmentName());
}
}
if (helpObject == null && !isTopicShown(name.getSegmentName())) {
helpObject= RHelpLtkUI.searchTopicObject1(help, name.getSegmentName(),
rNode, rSourceUnit );
if (helpObject == null && request.isValid()) {
doShow2(help.getREnv(), name.getSegmentName(), request);
return true;
}
}
}
}
catch (final CoreException e) {
}
finally {
help.unlock();
}
if (helpObject != null) {
return showHelpObject(helpObject, request);
}
return false;
}
private void doShow2(final REnv rEnv, final String topic, final RequestKey request) {
class BackgroundJob extends Job {
public BackgroundJob() {
super(String.format("Lookup R Help for '%1$s'", topic));
setPriority(Job.SHORT);
setUser(request.isExplicite());
}
@Override
protected IStatus run(final IProgressMonitor monitor) {
final ProgressMonitor m= EStatusUtils.convert(monitor);
try {
final REnvHelp help= RCore.getRHelpManager().getHelp(rEnv);
if (help != null) {
Object helpObject= null;
try {
helpObject= RHelpLtkUI.searchTopicObject2(help, topic, m);
}
catch (final StatusException e) {
final IStatus status= EStatusUtils.convert(e.getStatus());
showMessage(status, request);
return status;
}
finally {
help.unlock();
}
if (helpObject != null) {
showHelpObject(helpObject, request);
return Status.OK_STATUS;
}
if (request.isExplicite()) {
final IStatus status= new Status(IStatus.INFO, RUI.BUNDLE_ID, "No related help found.");
showMessage(status, request);
return status;
}
}
return Status.OK_STATUS;
}
finally {
RHelpView.this.requestSync.deleteRequest(request);
}
}
@Override
protected void canceling() {
RHelpView.this.requestSync.deleteRequest(request);
}
}
final Job job= new BackgroundJob();
job.schedule();
request.setAsync(job);
if (request.isExplicite()) {
final IProgressService progressService= getSite().getService(IProgressService.class);
if (progressService != null) {
progressService.showInDialog(getSite().getShell(), job);
}
}
}
private boolean isTopicShown(final String topic) {
final IPage currentPage= getCurrentPage();
if (currentPage instanceof RHelpViewPage) {
final Object helpObject= ((RHelpViewPage) currentPage).getHelpObject();
if (helpObject instanceof RHelpPage) {
return ((RHelpPage) helpObject).getTopics().contains(topic);
}
}
return false;
}
private boolean isLinkedValid() {
return (this.linkedEditor != null
&& this.linkedEditor == this.linkedEditor.getSite().getPage().getActiveEditor());
}
private boolean showHelpObject(final Object helpObject, final RequestKey request) {
final URI httpUrl= RCore.getRHelpHttpService().toHttpUrl(helpObject,
RHelpHttpService.BROWSE_TARGET );
if (httpUrl == null) {
return false;
}
final String urlString= httpUrl.toString();
UIAccess.getDisplay().asyncExec(new Runnable() {
@Override
public void run() {
if (isValid(request) && UIAccess.isOkToUse(getPageBook())) {
final BrowserSession session= getCurrentSession();
if (session == null || !urlString.equals(session.getUrl())) {
openUrl(urlString, session);
}
}
}
});
return true;
}
public boolean isValid(final RequestKey request) {
if (request.isExplicite()) {
return request.isValid();
}
else {
return (isLinkedValid() && request.isValid());
}
}
private void showMessage(final IStatus status, final RequestKey request) {
if (!request.isExplicite()) {
return;
}
UIAccess.getDisplay().asyncExec(new Runnable() {
@Override
public void run() {
if (isValid(request) && UIAccess.isOkToUse(getPageBook())) {
getStatusManager().setMessage(status, 10);
}
}
});
}
@Override
@SuppressWarnings("unchecked")
public <T> @Nullable T getAdapter(final Class<T> adapterType) {
if (adapterType == IShowInTarget.class) {
return (T) this;
}
return super.getAdapter(adapterType);
}
@Override
public boolean show(final ShowInContext context) {
final ISelection selection= context.getSelection();
if (selection instanceof LTKInputData) {
final LTKInputData state= (LTKInputData) selection;
return show(state, this.requestSync.newRequest(true));
}
return false;
}
}