Bug 574748: [R-Console] Add support for 'pager' in R
  - Add tool command handler for 'r/showContents'

Change-Id: Iab97a628265bff7ff31b1470bd8759a949cab9d0
diff --git a/r/org.eclipse.statet.r.console.ui/src/org/eclipse/statet/internal/r/console/ui/launching/RConsoleOptionsTab.java b/r/org.eclipse.statet.r.console.ui/src/org/eclipse/statet/internal/r/console/ui/launching/RConsoleOptionsTab.java
index 9933207..07cadca 100644
--- a/r/org.eclipse.statet.r.console.ui/src/org/eclipse/statet/internal/r/console/ui/launching/RConsoleOptionsTab.java
+++ b/r/org.eclipse.statet.r.console.ui/src/org/eclipse/statet/internal/r/console/ui/launching/RConsoleOptionsTab.java
@@ -103,9 +103,10 @@
 	static final TrackingConfiguration2LaunchConfiguration TRACKING_UTIL= new TrackingConfiguration2LaunchConfiguration();
 	
 	static final String ATTR_INTEGRATION_ROOT= "org.eclipse.statet.r.debug/integration"; //$NON-NLS-1$
-	static final String ATTR_INTEGRATION_RHELP_ENABLED= ATTR_INTEGRATION_ROOT+"integration.rhelp.enabled"; //$NON-NLS-1$
-	static final String ATTR_INTEGRATION_RGRAPHICS_ASDEFAULT= ATTR_INTEGRATION_ROOT+"integration.rgraphics.asdefault"; //$NON-NLS-1$
-	static final String ATTR_INTEGRATION_RDBGEXT_ENABLED= ATTR_INTEGRATION_ROOT+"integration.rdbgext.enabled"; //$NON-NLS-1$
+	static final String ATTR_INTEGRATION_RHELP_ENABLED= ATTR_INTEGRATION_ROOT + "integration.rhelp.enabled"; //$NON-NLS-1$
+	static final String ATTR_INTEGRATION_PAGER_ENABLED= ATTR_INTEGRATION_ROOT + "integration.pager.enabled"; //$NON-NLS-1$
+	static final String ATTR_INTEGRATION_RGRAPHICS_ASDEFAULT= ATTR_INTEGRATION_ROOT + "integration.rgraphics.asdefault"; //$NON-NLS-1$
+	static final String ATTR_INTEGRATION_RDBGEXT_ENABLED= ATTR_INTEGRATION_ROOT + "integration.rdbgext.enabled"; //$NON-NLS-1$
 	
 	
 	private final IObservableValue<Boolean> pinValue;
@@ -116,6 +117,7 @@
 	private final IObservableValue<String> startupSnippetValue;
 	
 	private final IObservableValue<Boolean> rHelpByStatetValue;
+	private final IObservableValue<Boolean> pagerByStatetValue;
 	private final IObservableValue<Boolean> rGraphicsByStatetValue;
 	private final IObservableValue<Boolean> rDbgExtValue;
 	
@@ -133,6 +135,7 @@
 	private SnippetEditor1 startupSnippetEditor;
 	
 	private Button rHelpByStatetControl;
+	private Button pagerByStatetControl;
 	private Button rGraphicsByStatetControl;
 	private Button rDbgExtControl;
 	
@@ -149,6 +152,7 @@
 		this.trackingEnabledSet= new WritableSet<>(realm, new HashSet<>(), TrackingConfiguration.class);
 		this.startupSnippetValue= new WritableValue<>(realm, "", String.class); //$NON-NLS-1$
 		this.rHelpByStatetValue= new WritableValue<>(realm, false, Boolean.TYPE);
+		this.pagerByStatetValue= new WritableValue<>(realm, false, Boolean.TYPE);
 		this.rGraphicsByStatetValue= new WritableValue<>(realm, false, Boolean.TYPE);
 		this.rDbgExtValue= new WritableValue<>(realm, false, Boolean.TYPE);
 		this.objectDBEnabledValue= new WritableValue<>(realm, false, Boolean.TYPE);
@@ -308,7 +312,12 @@
 		{	this.rHelpByStatetControl= new Button(container, SWT.CHECK);
 			final GridData gd= new GridData(SWT.FILL, SWT.FILL, true, false, 2, 1);
 			this.rHelpByStatetControl.setLayoutData(gd);
-			this.rHelpByStatetControl.setText("Enable R Help by StatET for help functions in R ('help', 'help.start', '?')");
+			this.rHelpByStatetControl.setText("Enable integrated R Help for help functions in R ('help', 'help.start', '?')");
+		}
+		{	this.pagerByStatetControl= new Button(container, SWT.CHECK);
+			final GridData gd= new GridData(SWT.FILL, SWT.FILL, true, false, 2, 1);
+			this.pagerByStatetControl.setLayoutData(gd);
+			this.pagerByStatetControl.setText("Enable integrated Pager view as default 'pager' to display text files ('file.show', ...)");
 		}
 		{	this.rGraphicsByStatetControl= new Button(container, SWT.CHECK);
 			final GridData gd= new GridData(SWT.FILL, SWT.FILL, true, false, 2, 1);
@@ -378,6 +387,10 @@
 				this.rHelpByStatetValue );
 		dbc.bindValue(
 				WidgetProperties.buttonSelection()
+				.observe(this.pagerByStatetControl),
+				this.pagerByStatetValue );
+		dbc.bindValue(
+				WidgetProperties.buttonSelection()
 						.observe(this.rGraphicsByStatetControl),
 				this.rGraphicsByStatetValue );
 		dbc.bindValue(
@@ -448,6 +461,9 @@
 		this.rHelpByStatetValue.setValue(readAttribute(configuration,
 				ATTR_INTEGRATION_RHELP_ENABLED,
 				true ));
+		this.pagerByStatetValue.setValue(readAttribute(configuration,
+				ATTR_INTEGRATION_RHELP_ENABLED,
+				true ));
 		this.rGraphicsByStatetValue.setValue(readAttribute(configuration,
 				ATTR_INTEGRATION_RGRAPHICS_ASDEFAULT,
 				true ));
@@ -530,6 +546,8 @@
 		
 		configuration.setAttribute(ATTR_INTEGRATION_RHELP_ENABLED,
 				this.rHelpByStatetValue.getValue().booleanValue() );
+		configuration.setAttribute(ATTR_INTEGRATION_PAGER_ENABLED,
+				this.pagerByStatetValue.getValue().booleanValue() );
 		configuration.setAttribute(ATTR_INTEGRATION_RGRAPHICS_ASDEFAULT,
 				this.rGraphicsByStatetValue.getValue().booleanValue() );
 		configuration.setAttribute(ATTR_INTEGRATION_RDBGEXT_ENABLED,
diff --git a/r/org.eclipse.statet.r.console.ui/src/org/eclipse/statet/internal/r/console/ui/launching/RConsoleRJLaunchDelegate.java b/r/org.eclipse.statet.r.console.ui/src/org/eclipse/statet/internal/r/console/ui/launching/RConsoleRJLaunchDelegate.java
index be0520c..9cd044b 100644
--- a/r/org.eclipse.statet.r.console.ui/src/org/eclipse/statet/internal/r/console/ui/launching/RConsoleRJLaunchDelegate.java
+++ b/r/org.eclipse.statet.r.console.ui/src/org/eclipse/statet/internal/r/console/ui/launching/RConsoleRJLaunchDelegate.java
@@ -80,7 +80,6 @@
 import org.eclipse.statet.rj.data.UnexpectedRDataException;
 import org.eclipse.statet.rj.renv.core.REnvConfiguration;
 import org.eclipse.statet.rj.server.RjsComConfig;
-import org.eclipse.statet.rj.services.FunctionCall;
 import org.eclipse.statet.rj.services.RVersion;
 
 
@@ -99,15 +98,18 @@
 		
 		private final Tool tool;
 		private final boolean enableRHelp;
+		private final boolean enablePager;
 		private final boolean enableRGraphics;
 		private final boolean enableRDbgExt;
 		private final boolean enableRDbg;
 		
 		public ConfigRunnable(final Tool tool, final boolean enableRHelp,
+				final boolean enablePager,
 				final boolean enableRGraphics,
 				final boolean enableRDbgExt, final boolean enableRDbg) {
 			this.tool= tool;
 			this.enableRHelp= enableRHelp;
+			this.enablePager= enablePager;
 			this.enableRGraphics= enableRGraphics;
 			this.enableRDbgExt= enableRDbgExt;
 			this.enableRDbg= enableRDbg;
@@ -150,14 +152,30 @@
 					r.evalVoid("library('rj', quietly= TRUE)", m); //$NON-NLS-1$
 				}
 				if (this.enableRHelp) {
-					final FunctionCall fcall= r.createFunctionCall(".statet.initHelp"); //$NON-NLS-1$
-					fcall.evalVoid(m);
+					r.createFunctionCall(".statet.initHelp") //$NON-NLS-1$
+							.evalVoid(m);
+				}
+				if (this.enablePager) {
+					try {
+						if (RDataUtils.checkSingleLogiValue(
+								r.createFunctionCall("exists") //$NON-NLS-1$
+									.addChar("x", "rj.pager") //$NON-NLS-1$ //$NON-NLS-2$
+									.addChar("where", "package:rj") //$NON-NLS-1$
+									.evalData(m) )) {
+							r.createFunctionCall("base::options") //$NON-NLS-1$
+									.add("pager", "rj::rj.pager") //$NON-NLS-1$ //$NON-NLS-2$
+									.evalVoid(m);
+						}
+					}
+					catch (final StatusException | UnexpectedRDataException e) {
+					}
 				}
 				if (this.enableRGraphics) {
 					try {
-						final FunctionCall fcall= r.createFunctionCall(".rj.initGD"); //$NON-NLS-1$
-						fcall.addLogi("default", true); //$NON-NLS-1$
-						RDataUtils.checkSingleLogiValue(fcall.evalData(m));
+						RDataUtils.checkSingleLogiValue(
+								r.createFunctionCall(".rj.initGD") //$NON-NLS-1$
+										.addLogi("default", true) //$NON-NLS-1$
+										.evalData(m) );
 					}
 					catch (final StatusException | UnexpectedRDataException e) {
 						r.handleStatus(new WarningStatus(RConsoleUIPlugin.BUNDLE_ID,
@@ -165,10 +183,10 @@
 								m );
 					}
 				}
-				{	final FunctionCall fcall= r.createFunctionCall(".statet.initDebug"); //$NON-NLS-1$
-					fcall.addLogi("mode", this.enableRDbg); //$NON-NLS-1$
-					fcall.addLogi("ext", this.enableRDbgExt); //$NON-NLS-1$
-					fcall.evalVoid(m);
+				{	r.createFunctionCall(".statet.initDebug") //$NON-NLS-1$
+							.addLogi("mode", this.enableRDbg) //$NON-NLS-1$
+							.addLogi("ext", this.enableRDbgExt) //$NON-NLS-1$
+							.evalVoid(m);
 				}
 			}
 			finally {
@@ -197,6 +215,7 @@
 		controller.addStartupRunnable(new ConfigRunnable(
 				controller.getTool(),
 				(rEnvConfig != null && configuration.getAttribute(RConsoleOptionsTab.ATTR_INTEGRATION_RHELP_ENABLED, true)),
+				configuration.getAttribute(RConsoleOptionsTab.ATTR_INTEGRATION_PAGER_ENABLED, true),
 				configuration.getAttribute(RConsoleOptionsTab.ATTR_INTEGRATION_RGRAPHICS_ASDEFAULT, true),
 				configuration.getAttribute(RConsoleOptionsTab.ATTR_INTEGRATION_RDBGEXT_ENABLED, true),
 				mode.equals(ILaunchManager.DEBUG_MODE) ));
diff --git a/r/org.eclipse.statet.r.console.ui/src/org/eclipse/statet/r/console/ui/launching/RConsoleLaunching.java b/r/org.eclipse.statet.r.console.ui/src/org/eclipse/statet/r/console/ui/launching/RConsoleLaunching.java
index d0e64db..ba3683c 100644
--- a/r/org.eclipse.statet.r.console.ui/src/org/eclipse/statet/r/console/ui/launching/RConsoleLaunching.java
+++ b/r/org.eclipse.statet.r.console.ui/src/org/eclipse/statet/r/console/ui/launching/RConsoleLaunching.java
@@ -32,6 +32,7 @@
 import org.eclipse.statet.r.core.RUtil;
 import org.eclipse.statet.r.ui.dataeditor.ShowRElementCommandHandler;
 import org.eclipse.statet.r.ui.graphics.RGraphicCommandHandler;
+import org.eclipse.statet.r.ui.pager.RShowContentsCommandHandler;
 import org.eclipse.statet.r.ui.pkgmanager.RPkgUICommandHandler;
 import org.eclipse.statet.r.ui.rhelp.RHelpUICommandHandler;
 
@@ -134,6 +135,9 @@
 		{	final ToolCommandHandler handler = new RGraphicCommandHandler();
 			controller.addCommandHandler(AbstractRController.INIT_RGRAPHIC_FACTORY_HANDLER_ID, handler);
 		}
+		{	final ToolCommandHandler handler= new RShowContentsCommandHandler();
+			controller.addCommandHandler(RShowContentsCommandHandler.SHOW_CONTENTS_COMMAND_ID, handler);
+		}
 		{	final ToolCommandHandler handler = new RHelpUICommandHandler();
 			controller.addCommandHandler(RHelpUICommandHandler.SHOW_HELP_COMMAND_ID, handler);
 			controller.addCommandHandler(RHelpUICommandHandler.SHOW_HELP_COMMAND_OLD_ID, handler);
diff --git a/r/org.eclipse.statet.r.ui/META-INF/MANIFEST.MF b/r/org.eclipse.statet.r.ui/META-INF/MANIFEST.MF
index 43b5297..0483cb3 100644
--- a/r/org.eclipse.statet.r.ui/META-INF/MANIFEST.MF
+++ b/r/org.eclipse.statet.r.ui/META-INF/MANIFEST.MF
@@ -79,6 +79,7 @@
  org.eclipse.statet.r.ui.editors,
  org.eclipse.statet.r.ui.editors.templates,
  org.eclipse.statet.r.ui.graphics,
+ org.eclipse.statet.r.ui.pager,
  org.eclipse.statet.r.ui.pkgmanager,
  org.eclipse.statet.r.ui.rhelp,
  org.eclipse.statet.r.ui.rtool,
diff --git a/r/org.eclipse.statet.r.ui/icons/view_16/pager.png b/r/org.eclipse.statet.r.ui/icons/view_16/pager.png
new file mode 100644
index 0000000..0f1a378
--- /dev/null
+++ b/r/org.eclipse.statet.r.ui/icons/view_16/pager.png
Binary files differ
diff --git a/r/org.eclipse.statet.r.ui/icons/view_16/pager@2x.png b/r/org.eclipse.statet.r.ui/icons/view_16/pager@2x.png
new file mode 100644
index 0000000..f8fd7e4
--- /dev/null
+++ b/r/org.eclipse.statet.r.ui/icons/view_16/pager@2x.png
Binary files differ
diff --git a/r/org.eclipse.statet.r.ui/plugin.xml b/r/org.eclipse.statet.r.ui/plugin.xml
index 800583b..534d731 100644
--- a/r/org.eclipse.statet.r.ui/plugin.xml
+++ b/r/org.eclipse.statet.r.ui/plugin.xml
@@ -2248,6 +2248,17 @@
             sequence="M1+-"/>
    </extension>
    
+   <!-- R Pager -->
+   <extension
+         point="org.eclipse.ui.editors">
+      <editor
+            id="org.eclipse.statet.r.editors.RPager"
+            icon="icons/r-graphic.png"
+            name="R Pager"
+            class="org.eclipse.statet.internal.r.ui.pager.RPagerEditor">
+      </editor>
+   </extension>
+   
    <!-- R Graphics -->
    <extension
          point="org.eclipse.ui.preferencePages">
diff --git a/r/org.eclipse.statet.r.ui/src/org/eclipse/statet/internal/r/ui/RUIPlugin.java b/r/org.eclipse.statet.r.ui/src/org/eclipse/statet/internal/r/ui/RUIPlugin.java
index fb7ea3d..79c4025 100644
--- a/r/org.eclipse.statet.r.ui/src/org/eclipse/statet/internal/r/ui/RUIPlugin.java
+++ b/r/org.eclipse.statet.r.ui/src/org/eclipse/statet/internal/r/ui/RUIPlugin.java
@@ -89,6 +89,8 @@
 	public static final String IMG_WIZBAN_NEW_RPROJECT= RUI.BUNDLE_ID + "/image/wizban/new.r_project"; //$NON-NLS-1$
 	public static final String IMG_WIZBAN_NEW_RPKGPROJECT= RUI.BUNDLE_ID + "/image/wizban/new.rpkg_project"; //$NON-NLS-1$
 	
+	public static final String PAGER_VIEW_IMAGE_ID= RUI.BUNDLE_ID + "/image/view/pager"; //$NON-NLS-1$
+	
 	public static final String IMG_LOCTOOL_FILTER_GENERAL= RUI.BUNDLE_ID + "/image/ltool/filter.general"; //$NON-NLS-1$
 	public static final String IMG_LOCTOOL_FILTER_LOCAL= RUI.BUNDLE_ID + "/image/ltool/filter.local"; //$NON-NLS-1$
 	
@@ -261,6 +263,8 @@
 		
 		util.register(RHelpUI.RHELP_VIEW_IMAGE_ID, ImageRegistryUtil.T_VIEW, "rhelp.png"); //$NON-NLS-1$
 		
+		util.register(PAGER_VIEW_IMAGE_ID, ImageRegistryUtil.T_VIEW, "pager.png"); //$NON-NLS-1$
+		
 		util.register(RUI.IMG_OBJ_COMMON_FUNCTION, ImageRegistryUtil.T_OBJ, "function.png"); //$NON-NLS-1$
 		util.register(RUI.IMG_OBJ_COMMON_LOCAL_FUNCTION, ImageRegistryUtil.T_OBJ, "function-local.png"); //$NON-NLS-1$
 		util.register(RUI.IMG_OBJ_GENERIC_FUNCTION, ImageRegistryUtil.T_OBJ, "generic_function.png"); //$NON-NLS-1$
diff --git a/r/org.eclipse.statet.r.ui/src/org/eclipse/statet/internal/r/ui/pager/RPagerEditor.java b/r/org.eclipse.statet.r.ui/src/org/eclipse/statet/internal/r/ui/pager/RPagerEditor.java
new file mode 100644
index 0000000..38947d1
--- /dev/null
+++ b/r/org.eclipse.statet.r.ui/src/org/eclipse/statet/internal/r/ui/pager/RPagerEditor.java
@@ -0,0 +1,140 @@
+/*=============================================================================#
+ # Copyright (c) 2021 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.pager;
+
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.ui.IEditorInput;
+import org.eclipse.ui.IEditorPart;
+import org.eclipse.ui.PartInitException;
+import org.eclipse.ui.part.MultiPageEditorPart;
+
+import org.eclipse.statet.jcommons.collections.ImList;
+import org.eclipse.statet.jcommons.lang.NonNullByDefault;
+
+import org.eclipse.statet.ecommons.text.core.util.ImmutableDocument;
+
+import org.eclipse.statet.internal.r.ui.RUIPlugin;
+import org.eclipse.statet.internal.r.ui.pager.RPagerEditorInput.TextFile;
+import org.eclipse.statet.ltk.core.input.BasicSourceFragment;
+import org.eclipse.statet.r.ui.RUI;
+
+
+@NonNullByDefault
+public class RPagerEditor extends MultiPageEditorPart {
+	
+	
+	public RPagerEditor() {
+	}
+	
+	
+	@Override
+	protected Image getDefaultImage() {
+		return RUI.getImage(RUIPlugin.PAGER_VIEW_IMAGE_ID);
+	}
+	
+	
+	@Override
+	protected void setInputWithNotify(final IEditorInput input) {
+		super.setInput(input);
+		
+		setPartName(input.getName());
+		
+		if (getContainer() != null) {
+			updatePages();
+		}
+		
+		firePropertyChange(IEditorPart.PROP_INPUT);
+	}
+	
+	@Override
+	protected void setInput(final IEditorInput input) {
+		setInputWithNotify(input);
+	}
+	
+	
+	protected void updatePages() {
+		while (true) {
+			final int pageCount= getPageCount();
+			if (pageCount == 0) {
+				break;
+			}
+			removePage(pageCount - 1);
+		}
+		
+		final IEditorInput editorInput= getEditorInput();
+		if (editorInput instanceof RPagerEditorInput) {
+			final var pagerInput= (RPagerEditorInput)editorInput;
+			final ImList<TextFile> files= pagerInput.getFiles();
+			int id= 0;
+			for (final var textFile : files) {
+				try {
+					final var sourceFragment= new BasicSourceFragment("RPagerFile#" + id++,
+							textFile.getName(), textFile.getName(),
+							new ImmutableDocument(textFile.getContent(), 0) );
+					final TextFilePage page= new TextFilePage();
+					addPage(page, new TextFileEditorInput(sourceFragment));
+				}
+				catch (final Exception e) {
+					RUIPlugin.logError(String.format("An error occurred when creating R Pager editor page for '%1$s'.",
+							textFile.getName() ),
+							e );
+				}
+			}
+		}
+	}
+	
+	@Override
+	protected void createPages() {
+		updatePages();
+	}
+	
+	@Override
+	public void addPage(final int index, final IEditorPart editor, final IEditorInput input)
+			throws PartInitException {
+		super.addPage(index, editor, input);
+		setPageText(index, input.getName());
+	}
+	
+	
+	@Override
+	public boolean isSaveAsAllowed() {
+		final int pageIndex= getActivePage();
+		if (pageIndex >= 0) {
+			final IEditorPart editor= getEditor(pageIndex);
+			if (editor != null) {
+				return true;
+			}
+		}
+		return false;
+	}
+	
+	@Override
+	public void doSave(final IProgressMonitor monitor) {
+	}
+	
+	@Override
+	public void doSaveAs() {
+		final int pageIndex= getActivePage();
+		if (pageIndex >= 0) {
+			final IEditorPart editor= getEditor(pageIndex);
+			if (editor != null) {
+				editor.doSaveAs();
+				return;
+			}
+		}
+	}
+	
+}
diff --git a/r/org.eclipse.statet.r.ui/src/org/eclipse/statet/internal/r/ui/pager/RPagerEditorInput.java b/r/org.eclipse.statet.r.ui/src/org/eclipse/statet/internal/r/ui/pager/RPagerEditorInput.java
new file mode 100644
index 0000000..1bf4035
--- /dev/null
+++ b/r/org.eclipse.statet.r.ui/src/org/eclipse/statet/internal/r/ui/pager/RPagerEditorInput.java
@@ -0,0 +1,105 @@
+/*=============================================================================#
+ # Copyright (c) 2021 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.pager;
+
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.ui.IEditorInput;
+import org.eclipse.ui.IPersistableElement;
+
+import org.eclipse.statet.jcommons.collections.ImList;
+import org.eclipse.statet.jcommons.lang.NonNullByDefault;
+import org.eclipse.statet.jcommons.lang.Nullable;
+import org.eclipse.statet.jcommons.ts.core.Tool;
+
+import org.eclipse.statet.r.ui.RUI;
+import org.eclipse.statet.rj.ts.core.RTool;
+
+
+@NonNullByDefault
+public class RPagerEditorInput implements IEditorInput {
+	
+	
+	public static class TextFile {
+		
+		private final String name;
+		private final String content;
+		
+		public TextFile(final String name, final String content) {
+			this.name= name;
+			this.content= content;
+		}
+		
+		public String getName() {
+			return this.name;
+		}
+		
+		public String getContent() {
+			return this.content;
+		}
+		
+	}
+	
+	
+	private final String name;
+	private final RTool source;
+	
+	private final ImList<TextFile> files;
+	
+	
+	public RPagerEditorInput(final String name, final RTool source,
+			final ImList<TextFile> files) {
+		this.name= name;
+		this.source= source;
+		this.files= files;
+	}
+	
+	
+	@Override
+	public boolean exists() {
+		return true;
+	}
+	
+	@Override
+	public @Nullable ImageDescriptor getImageDescriptor() {
+		return RUI.getImageDescriptor(RUI.RD_EDITOR_ID);
+	}
+	
+	@Override
+	public String getName() {
+		return this.name;
+	}
+	
+	@Override
+	public String getToolTipText() {
+		return this.name + '\n' + this.source.getLabel(Tool.LONG_LABEL);
+	}
+	
+	
+	public ImList<TextFile> getFiles() {
+		return this.files;
+	}
+	
+	@Override
+	public @Nullable IPersistableElement getPersistable() {
+		return null;
+	}
+	
+	
+	@Override
+	public <T> @Nullable T getAdapter(final Class<T> adapter) {
+		return null;
+	}
+	
+}
diff --git a/r/org.eclipse.statet.r.ui/src/org/eclipse/statet/internal/r/ui/pager/TextFileEditorInput.java b/r/org.eclipse.statet.r.ui/src/org/eclipse/statet/internal/r/ui/pager/TextFileEditorInput.java
new file mode 100644
index 0000000..af10973
--- /dev/null
+++ b/r/org.eclipse.statet.r.ui/src/org/eclipse/statet/internal/r/ui/pager/TextFileEditorInput.java
@@ -0,0 +1,32 @@
+/*=============================================================================#
+ # Copyright (c) 2021 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.pager;
+
+import org.eclipse.statet.jcommons.lang.NonNullByDefault;
+
+import org.eclipse.statet.ltk.core.input.SourceFragment;
+import org.eclipse.statet.ltk.ui.input.BasicSourceFragmentEditorInput;
+
+
+@NonNullByDefault
+public class TextFileEditorInput extends BasicSourceFragmentEditorInput<SourceFragment> {
+	
+	
+	public TextFileEditorInput(final SourceFragment fragment) {
+		super(fragment);
+	}
+	
+	
+}
diff --git a/r/org.eclipse.statet.r.ui/src/org/eclipse/statet/internal/r/ui/pager/TextFilePage.java b/r/org.eclipse.statet.r.ui/src/org/eclipse/statet/internal/r/ui/pager/TextFilePage.java
new file mode 100644
index 0000000..9095d66
--- /dev/null
+++ b/r/org.eclipse.statet.r.ui/src/org/eclipse/statet/internal/r/ui/pager/TextFilePage.java
@@ -0,0 +1,63 @@
+/*=============================================================================#
+ # Copyright (c) 2021 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.pager;
+
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.jface.text.source.CompositeRuler;
+import org.eclipse.jface.text.source.IVerticalRuler;
+import org.eclipse.ui.texteditor.IDocumentProvider;
+import org.eclipse.ui.texteditor.StatusTextEditor;
+
+import org.eclipse.statet.jcommons.lang.NonNullByDefault;
+import org.eclipse.statet.jcommons.lang.Nullable;
+
+import org.eclipse.statet.ltk.ui.sourceediting.FragmentDocumentProvider;
+
+
+@NonNullByDefault
+public class TextFilePage extends StatusTextEditor {
+	
+	
+	private final IDocumentProvider documentProvider= new FragmentDocumentProvider("", null);
+	
+	
+	public TextFilePage() {
+		setDocumentProvider(this.documentProvider);
+		setRulerContextMenuId(""); //$NON-NLS-1$
+	}
+	
+	
+	@Override
+	public boolean isDirty() {
+		return false;
+	}
+	
+	@Override
+	public boolean isSaveAsAllowed() {
+		return false;
+	}
+	
+	@Override
+	protected void performSaveAs(final @Nullable IProgressMonitor progressMonitor) {
+		// for save as: implement and change isSaveAsAllowed to true
+	}
+	
+	
+	@Override
+	protected IVerticalRuler createVerticalRuler() {
+		return new CompositeRuler();
+	}
+	
+}
diff --git a/r/org.eclipse.statet.r.ui/src/org/eclipse/statet/r/ui/pager/RShowContentsCommandHandler.java b/r/org.eclipse.statet.r.ui/src/org/eclipse/statet/r/ui/pager/RShowContentsCommandHandler.java
new file mode 100644
index 0000000..a3661a2
--- /dev/null
+++ b/r/org.eclipse.statet.r.ui/src/org/eclipse/statet/r/ui/pager/RShowContentsCommandHandler.java
@@ -0,0 +1,94 @@
+/*=============================================================================#
+ # Copyright (c) 2012, 2021 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.r.ui.pager;
+
+import static org.eclipse.statet.jcommons.lang.ObjectUtils.NonNull_StringArray_TYPE;
+import static org.eclipse.statet.jcommons.lang.ObjectUtils.Nullable_StringArray_TYPE;
+import static org.eclipse.statet.jcommons.lang.ObjectUtils.nonNullAssert;
+
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.ui.ide.IDE;
+
+import org.eclipse.statet.jcommons.collections.ImCollections;
+import org.eclipse.statet.jcommons.lang.NonNull;
+import org.eclipse.statet.jcommons.lang.NonNullByDefault;
+import org.eclipse.statet.jcommons.status.ErrorStatus;
+import org.eclipse.statet.jcommons.status.ProgressMonitor;
+import org.eclipse.statet.jcommons.status.Status;
+import org.eclipse.statet.jcommons.status.StatusException;
+import org.eclipse.statet.jcommons.ts.core.ToolCommandData;
+import org.eclipse.statet.jcommons.util.StringUtils;
+
+import org.eclipse.statet.ecommons.ui.util.UIAccess;
+
+import org.eclipse.statet.internal.r.ui.pager.RPagerEditorInput;
+import org.eclipse.statet.nico.ui.NicoUI;
+import org.eclipse.statet.r.ui.RUI;
+import org.eclipse.statet.rj.ts.core.AbstractRToolCommandHandler;
+import org.eclipse.statet.rj.ts.core.RToolService;
+
+
+@NonNullByDefault
+public class RShowContentsCommandHandler extends AbstractRToolCommandHandler {
+	
+	
+	public static final String SHOW_CONTENTS_COMMAND_ID= "r/showContents"; //$NON-NLS-1$
+	
+	
+	public RShowContentsCommandHandler() {
+	}
+	
+	
+	@Override
+	public Status execute(final String id, final RToolService r, final ToolCommandData data,
+			final ProgressMonitor m) throws StatusException {
+		switch (id) {
+		case SHOW_CONTENTS_COMMAND_ID:
+			{	final var contents= data.getRequired("contents", NonNull_StringArray_TYPE);
+				var title= data.getStringRequired("title");
+				final var headers= data.getRequired("headers", Nullable_StringArray_TYPE);
+				
+				title= StringUtils.toSimpleSingleLine(title);
+				final var files= new RPagerEditorInput. @NonNull TextFile[contents.length];
+				for (int i= 0; i < files.length; i++) {
+					String header= headers[i];
+					if (header == null) {
+						header= "#" + (i + 1);
+					}
+					files[i]= new RPagerEditorInput.TextFile(header, nonNullAssert(contents[i]));
+				}
+				
+				final var editorInput= new RPagerEditorInput(title, r.getTool(),
+						ImCollections.newList(files) );
+				try {
+					UIAccess.checkedSyncExec(() -> {
+						final var workbenchPage= NicoUI.getToolRegistry().findWorkbenchPage(r.getTool());
+						IDE.openEditor(workbenchPage, editorInput, "org.eclipse.statet.r.editors.RPager", true);
+					});
+				}
+				catch (final CoreException e) {
+					throw new StatusException(new ErrorStatus(RUI.BUNDLE_ID, 0,
+							"An error occurred when opening the R Pager view.",
+							e ));
+				}
+				
+				return Status.OK_STATUS;
+			}
+		default:
+			throw new UnsupportedOperationException();
+		}
+	}
+	
+}