1
0
Fork 0
mirror of https://github.com/eclipse-cdt/cdt synced 2025-04-21 21:52:10 +02:00

Bug 563015: terminal: open files/links with ctrl-click

- hover with ctrl+mouse underlines word under cursor
- ctrl-click tries to open the word:
  - if a relative path (not starting with /) a full path is
    obtained by prepending the shell cwd
  - if the fullpath maps to a workspace file, it is opened
  - otherwise open the OpenResource dialog with the word as
    filter text
  - if there is line/column information (separated by colons)
    then the opened editor jumps to that line
- http and https words are opened in a browser window

Change-Id: I3f46accbf1eac6743d7b0c3b34bf30ac5e7523bb
Signed-off-by: Fabrizio Iannetti <fabrizio.iannetti@gmail.com>
Also-by: Jonah Graham <jonah@kichwacoders.com>
Signed-off-by: Jonah Graham <jonah@kichwacoders.com>
This commit is contained in:
Fabrizio Iannetti 2021-03-04 14:22:02 -05:00 committed by Jonah Graham
parent 104819751c
commit e6d5c634b9
23 changed files with 761 additions and 9 deletions

View file

@ -286,6 +286,11 @@ public class Spawner extends Process {
}
}
@Override
public long pid() {
return pid;
}
/**
* On Windows, interrupt the spawned program by using Cygwin's utility 'kill -SIGINT' if it's a Cgywin
* program, otherwise send it a CTRL-C. If Cygwin's 'kill' command is not available, send a CTRL-C. On

View file

@ -2,7 +2,7 @@ Manifest-Version: 1.0
Bundle-ManifestVersion: 2
Bundle-Name: %pluginName
Bundle-SymbolicName: org.eclipse.tm.terminal.connector.process;singleton:=true
Bundle-Version: 4.7.0.qualifier
Bundle-Version: 4.8.0.qualifier
Bundle-Activator: org.eclipse.tm.terminal.connector.process.activator.UIPlugin
Bundle-Vendor: %providerName
Import-Package: org.eclipse.cdt.utils.pty;mandatory:=native,

View file

@ -16,8 +16,12 @@ import java.io.File;
import java.io.IOException;
import java.io.StreamTokenizer;
import java.io.StringReader;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import org.eclipse.cdt.utils.pty.PTY;
import org.eclipse.cdt.utils.spawner.ProcessFactory;
@ -32,6 +36,7 @@ import org.eclipse.tm.internal.terminal.provisional.api.ISettingsStore;
import org.eclipse.tm.internal.terminal.provisional.api.ITerminalControl;
import org.eclipse.tm.internal.terminal.provisional.api.NullSettingsStore;
import org.eclipse.tm.internal.terminal.provisional.api.TerminalState;
import org.eclipse.tm.terminal.connector.process.activator.UIPlugin;
import org.eclipse.tm.terminal.connector.process.nls.Messages;
import org.eclipse.tm.terminal.view.core.interfaces.constants.ILineSeparatorConstants;
import org.eclipse.tm.terminal.view.core.utils.Env;
@ -296,4 +301,20 @@ public class ProcessConnector extends AbstractStreamsConnector {
}
}
/**
* @since 4.8
*/
@Override
public Optional<String> getWorkingDirectory() {
long pid = process.pid();
try {
if (Platform.getOS().equals(Platform.OS_LINUX)) {
Path procCwd = Files.readSymbolicLink(FileSystems.getDefault().getPath("/proc/" + pid + "/cwd")); //$NON-NLS-1$//$NON-NLS-2$
return Optional.of(procCwd.toAbsolutePath().toString());
}
} catch (Exception e) {
UIPlugin.log("Failed to obtain working directory of process id " + pid, e); //$NON-NLS-1$
}
return Optional.empty();
}
}

View file

@ -11,6 +11,8 @@
*******************************************************************************/
package org.eclipse.tm.terminal.connector.process.activator;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.jface.resource.ImageRegistry;
import org.eclipse.swt.graphics.Image;
@ -103,4 +105,13 @@ public class UIPlugin extends AbstractUIPlugin {
public static ImageDescriptor getImageDescriptor(String key) {
return getDefault().getImageRegistry().getDescriptor(key);
}
public static void log(String msg, Throwable e) {
log(new Status(IStatus.ERROR, getUniqueIdentifier(), IStatus.ERROR, msg, e));
}
public static void log(IStatus status) {
getDefault().getLog().log(status);
}
}

View file

@ -1,3 +1,4 @@
org.eclipse.tm.terminal.control/debug/log = false
org.eclipse.tm.terminal.control/debug/log/char = false
org.eclipse.tm.terminal.control/debug/log/VT100Backend = false
org.eclipse.tm.terminal.control/debug/log/hover = false

View file

@ -2,7 +2,7 @@ Manifest-Version: 1.0
Bundle-ManifestVersion: 2
Bundle-Name: %pluginName
Bundle-SymbolicName: org.eclipse.tm.terminal.control; singleton:=true
Bundle-Version: 5.1.0.qualifier
Bundle-Version: 5.2.0.qualifier
Bundle-Activator: org.eclipse.tm.internal.terminal.control.impl.TerminalPlugin
Bundle-Vendor: %providerName
Bundle-Localization: plugin

View file

@ -16,6 +16,7 @@
package org.eclipse.tm.internal.terminal.connector;
import java.io.OutputStream;
import java.util.Optional;
import org.eclipse.core.runtime.IAdaptable;
import org.eclipse.core.runtime.Platform;
@ -260,4 +261,12 @@ public class TerminalConnector implements ITerminalConnector {
// maybe we have to be adapted....
return Platform.getAdapterManager().getAdapter(this, adapter);
}
@Override
public Optional<String> getWorkingDirectory() {
if (fConnector != null) {
return fConnector.getWorkingDirectory();
}
return Optional.empty();
}
}

View file

@ -16,6 +16,7 @@ import org.eclipse.tm.terminal.model.ITerminalTextDataReadOnly;
/**
* Terminal specific version of {@link org.eclipse.swt.events.MouseListener}
* @since 4.1
* @see ITerminalMouseListener2
*/
public interface ITerminalMouseListener {
/**

View file

@ -0,0 +1,74 @@
/*******************************************************************************
* Copyright (c) 2021 Kichwa Coders Canada Inc. and others.
* 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
*******************************************************************************/
package org.eclipse.tm.internal.terminal.control;
import org.eclipse.tm.terminal.model.ITerminalTextDataReadOnly;
/**
* Extension of {@link ITerminalMouseListener} for consumers that need the stateMask for a button mouse action.
*
* If ITerminalMouseListener2 is used, the methods in ITerminalMouseListener will not be called.
*
* @since 5.2
* @see ITerminalMouseListener
*/
public interface ITerminalMouseListener2 extends ITerminalMouseListener {
/**
* Invoked when a double-click has happend inside the terminal control.<br>
* <br>
* <strong>Important:</strong> the event fires for every click, even outside the text region.
* @param terminalText a read-only view of the current terminal text
* @param button see {@link org.eclipse.swt.events.MouseEvent#button} for the meaning of the button values
* @param stateMask see {@link org.eclipse.swt.events.MouseEvent#stateMask} for the meaning of the values
*/
default void mouseDoubleClick(ITerminalTextDataReadOnly terminalText, int line, int column, int button,
int stateMask) {
// do nothing by default so that implementors only need to implement methods they care about
}
@Override
default void mouseDoubleClick(ITerminalTextDataReadOnly terminalText, int line, int column, int button) {
throw new UnsupportedOperationException();
}
/**
* Invoked when a mouse button is pushed down inside the terminal control.<br>
* <br>
* <strong>Important:</strong> the event fires for every mouse down, even outside the text region.
* @param terminalText a read-only view of the current terminal text
* @param button see {@link org.eclipse.swt.events.MouseEvent#button} for the meaning of the button values
* @param stateMask see {@link org.eclipse.swt.events.MouseEvent#stateMask} for the meaning of the values
*/
default void mouseDown(ITerminalTextDataReadOnly terminalText, int line, int column, int button, int stateMask) {
// do nothing by default so that implementors only need to implement methods they care about
}
@Override
default void mouseDown(ITerminalTextDataReadOnly terminalText, int line, int column, int button) {
throw new UnsupportedOperationException();
}
/**
* Invoked when a mouse button is released inside the terminal control.<br>
* <br>
* <strong>Important:</strong> the event fires for every mouse up, even outside the text region.
* @param terminalText a read-only view of the current terminal text
* @param button see {@link org.eclipse.swt.events.MouseEvent#button} for the meaning of the button values
* @param stateMask see {@link org.eclipse.swt.events.MouseEvent#stateMask} for the meaning of the values
*/
default void mouseUp(ITerminalTextDataReadOnly terminalText, int line, int column, int button, int stateMask) {
// do nothing by default so that implementors only need to implement methods they care about
}
@Override
default void mouseUp(ITerminalTextDataReadOnly terminalText, int line, int column, int button) {
throw new UnsupportedOperationException();
}
}

View file

@ -159,11 +159,13 @@ public interface ITerminalViewControl {
/**
* @since 4.1
* @param listener may be a {@link ITerminalMouseListener2} for extra callbacks
*/
void addMouseListener(ITerminalMouseListener listener);
/**
* @since 4.1
* @param listener may be a {@link ITerminalMouseListener2} for extra callbacks
*/
void removeMouseListener(ITerminalMouseListener listener);
@ -171,4 +173,9 @@ public interface ITerminalViewControl {
* @since 5.1
*/
void setTerminalTitle(String newTitle);
/**
* @since 5.2
*/
String getHoverSelection();
}

View file

@ -1405,4 +1405,9 @@ public class VT100TerminalControl implements ITerminalControlForText, ITerminalC
getCtlText().removeTerminalMouseListener(listener);
}
@Override
public String getHoverSelection() {
return fCtlText.getHoverSelection();
}
}

View file

@ -16,6 +16,7 @@
package org.eclipse.tm.internal.terminal.provisional.api;
import java.io.OutputStream;
import java.util.Optional;
import org.eclipse.core.runtime.IAdaptable;
import org.eclipse.tm.internal.terminal.control.ITerminalViewControl;
@ -146,4 +147,12 @@ public interface ITerminalConnector extends IAdaptable {
*/
String getSettingsSummary();
/**
* @return An optional with the absolute path if available of the current working dir, empty otherwise.
* @since 5.2
*/
default Optional<String> getWorkingDirectory() {
return Optional.empty();
}
}

View file

@ -47,14 +47,17 @@ public final class Logger {
public static final String TRACE_DEBUG_LOG = "org.eclipse.tm.terminal.control/debug/log"; //$NON-NLS-1$
public static final String TRACE_DEBUG_LOG_CHAR = "org.eclipse.tm.terminal.control/debug/log/char"; //$NON-NLS-1$
public static final String TRACE_DEBUG_LOG_VT100BACKEND = "org.eclipse.tm.terminal.control/debug/log/VT100Backend"; //$NON-NLS-1$
/** @since 5.2 */
public static final String TRACE_DEBUG_LOG_HOVER = "org.eclipse.tm.terminal.control/debug/log/hover"; //$NON-NLS-1$
private static PrintStream logStream;
static {
// Any of the three known debugging options turns on the creation of the log file
// Any of the known debugging options turns on the creation of the log file
boolean createLogFile = TerminalPlugin.isOptionEnabled(TRACE_DEBUG_LOG)
|| TerminalPlugin.isOptionEnabled(TRACE_DEBUG_LOG_CHAR)
|| TerminalPlugin.isOptionEnabled(TRACE_DEBUG_LOG_VT100BACKEND);
|| TerminalPlugin.isOptionEnabled(TRACE_DEBUG_LOG_VT100BACKEND)
|| TerminalPlugin.isOptionEnabled(TRACE_DEBUG_LOG_HOVER);
// Log only if tracing is enabled
if (createLogFile && TerminalPlugin.getDefault() != null) {

View file

@ -14,6 +14,7 @@
package org.eclipse.tm.internal.terminal.provisional.api.provider;
import java.io.OutputStream;
import java.util.Optional;
import org.eclipse.tm.internal.terminal.provisional.api.ISettingsStore;
import org.eclipse.tm.internal.terminal.provisional.api.ITerminalControl;
@ -151,4 +152,12 @@ public abstract class TerminalConnectorImpl {
*/
public void setTerminalSize(int newWidth, int newHeight) {
}
/**
* @since 5.2
*/
public Optional<String> getWorkingDirectory() {
return Optional.empty();
}
}

View file

@ -19,10 +19,14 @@ import java.util.Iterator;
import java.util.List;
import org.eclipse.swt.graphics.Point;
import org.eclipse.tm.internal.terminal.control.impl.TerminalPlugin;
import org.eclipse.tm.internal.terminal.provisional.api.Logger;
import org.eclipse.tm.terminal.model.ITerminalTextDataReadOnly;
import org.eclipse.tm.terminal.model.ITerminalTextDataSnapshot;
import org.eclipse.tm.terminal.model.TextRange;
abstract public class AbstractTextCanvasModel implements ITextCanvasModel {
private static final boolean DEBUG_HOVER = TerminalPlugin.isOptionEnabled(Logger.TRACE_DEBUG_LOG_HOVER);
protected List<ITextCanvasModelListener> fListeners = new ArrayList<>();
private int fCursorLine;
private int fCursorColumn;
@ -45,6 +49,8 @@ abstract public class AbstractTextCanvasModel implements ITextCanvasModel {
boolean fInUpdate;
private int fCols;
private TextRange fHoverRange = TextRange.EMPTY;
public AbstractTextCanvasModel(ITerminalTextDataSnapshot snapshot) {
fSnapshot = snapshot;
fLines = fSnapshot.getHeight();
@ -309,6 +315,98 @@ abstract public class AbstractTextCanvasModel implements ITextCanvasModel {
return fCurrentSelection;
}
@Override
public boolean hasHoverSelection(int line) {
if (fHoverRange.isEmpty()) {
return false;
}
return fHoverRange.contains(line);
}
@Override
public Point getHoverSelectionStart() {
if (!fHoverRange.isEmpty()) {
return fHoverRange.getStart();
}
return null;
}
@Override
public Point getHoverSelectionEnd() {
// Note - to match behaviour of getSelectionEnd this method
// returns the inclusive end. As the fHoverRange is exclusive
// we need to decrement the end positions before returning them.
if (!fHoverRange.isEmpty()) {
Point end = fHoverRange.getEnd();
end.x--;
end.y--;
return end;
}
return null;
}
@Override
public void expandHoverSelectionAt(final int line, final int col) {
if (fHoverRange.contains(col, line)) {
// position is inside current hover range -> no change
return;
}
fHoverRange = TextRange.EMPTY;
if (line < 0 || line > fSnapshot.getHeight() || col < 0) {
return;
}
int row1 = line;
int row2 = line;
while (row1 > 0 && fSnapshot.isWrappedLine(row1 - 1))
row1--;
while (row2 < fSnapshot.getHeight() && fSnapshot.isWrappedLine(row2))
row2++;
row2++;
String lineText = ""; //$NON-NLS-1$
for (int l = row1; l < row2; l++) {
char[] chars = fSnapshot.getChars(l);
if (chars == null)
return;
lineText += String.valueOf(chars);
}
int width = fSnapshot.getWidth();
int col1 = col + (line - row1) * width;
if (lineText.length() <= col1 || isBoundaryChar(lineText.charAt(col1))) {
return;
}
int wordStart = 0;
int wordEnd = lineText.length();
for (int c = col1; c >= 1; c--) {
if (isBoundaryChar(lineText.charAt(c - 1))) {
wordStart = c;
break;
}
}
for (int c = col1; c < lineText.length(); c++) {
if (isBoundaryChar(lineText.charAt(c))) {
wordEnd = c;
break;
}
}
if (wordStart < wordEnd) {
fHoverRange = new TextRange(row1 + wordStart / width, row1 + (wordEnd - 1) / width + 1, (wordStart % width),
(wordEnd - 1) % width + 1, lineText.substring(wordStart, wordEnd));
if (DEBUG_HOVER) {
System.out.format("hover: %s <- [%s,%s][%s,%s]\n", //$NON-NLS-1$
fHoverRange, col, line, wordStart, wordEnd);
}
}
}
@Override
public String getHoverSelectionText() {
return fHoverRange.text;
}
private boolean isBoundaryChar(char c) {
return Character.isWhitespace(c) || (c < '\u0020') || c == '"' || c == '\'';
}
// helper to sanitize text copied out of a snapshot
private static String scrubLine(String text) {
// get rid of the empty space at the end of the lines

View file

@ -90,6 +90,45 @@ public interface ITextCanvasModel {
String getSelectedText();
/**
* Expand the hover selection to the word at the given position.
*
* @param line line
* @param col column
*/
void expandHoverSelectionAt(int line, int col);
/**
* @param line
* @return true if line is part of the hover selection
*/
boolean hasHoverSelection(int line);
/**
* Get the text of the current hover selection.
*
* @return the hover selection text, never null.
*/
String getHoverSelectionText();
/**
* Get the start of the hover selection.
*
* @return the start of the hover selection or null if nothing is selected
* {@link Point#x} is the column and {@link Point#y} is the line.
* Returns non-null if {@link #hasHoverSelection(int)} returns true
*/
Point getHoverSelectionStart();
/**
* Get the end of the hover selection (inclusive).
*
* @return the end of the hover selection or null if nothing is selected
* {@link Point#x} is the column and {@link Point#y} is the line.
* Returns non-null if {@link #hasHoverSelection(int)} returns true
*/
Point getHoverSelectionEnd();
/**
* Collect and return all text present in the model.
*
@ -100,4 +139,5 @@ public interface ITextCanvasModel {
* @since 4.4
*/
String getAllText();
}

View file

@ -43,6 +43,7 @@ import org.eclipse.swt.graphics.RGB;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.tm.internal.terminal.control.ITerminalMouseListener;
import org.eclipse.tm.internal.terminal.control.ITerminalMouseListener2;
import org.eclipse.tm.terminal.model.TerminalColor;
/**
@ -146,7 +147,13 @@ public class TextCanvas extends GridCanvas {
Point pt = screenPointToCell(e.x, e.y);
if (pt != null) {
for (ITerminalMouseListener l : fMouseListeners) {
l.mouseDoubleClick(fCellCanvasModel.getTerminalText(), pt.y, pt.x, e.button);
if (l instanceof ITerminalMouseListener2) {
ITerminalMouseListener2 l2 = (ITerminalMouseListener2) l;
l2.mouseDoubleClick(fCellCanvasModel.getTerminalText(), pt.y, pt.x, e.button,
e.stateMask);
} else {
l.mouseDoubleClick(fCellCanvasModel.getTerminalText(), pt.y, pt.x, e.button);
}
}
}
}
@ -170,7 +177,12 @@ public class TextCanvas extends GridCanvas {
Point pt = screenPointToCell(e.x, e.y);
if (pt != null) {
for (ITerminalMouseListener l : fMouseListeners) {
l.mouseDown(fCellCanvasModel.getTerminalText(), pt.y, pt.x, e.button);
if (l instanceof ITerminalMouseListener2) {
ITerminalMouseListener2 l2 = (ITerminalMouseListener2) l;
l2.mouseDown(fCellCanvasModel.getTerminalText(), pt.y, pt.x, e.button, e.stateMask);
} else {
l.mouseDown(fCellCanvasModel.getTerminalText(), pt.y, pt.x, e.button);
}
}
}
}
@ -190,7 +202,12 @@ public class TextCanvas extends GridCanvas {
Point pt = screenPointToCell(e.x, e.y);
if (pt != null) {
for (ITerminalMouseListener l : fMouseListeners) {
l.mouseUp(fCellCanvasModel.getTerminalText(), pt.y, pt.x, e.button);
if (l instanceof ITerminalMouseListener2) {
ITerminalMouseListener2 l2 = (ITerminalMouseListener2) l;
l2.mouseUp(fCellCanvasModel.getTerminalText(), pt.y, pt.x, e.button, e.stateMask);
} else {
l.mouseUp(fCellCanvasModel.getTerminalText(), pt.y, pt.x, e.button);
}
}
}
}
@ -200,7 +217,15 @@ public class TextCanvas extends GridCanvas {
if (fDraggingStart != null) {
updateHasSelection(e);
setSelection(screenPointToCell(e.x, e.y));
fCellCanvasModel.expandHoverSelectionAt(-1, -1);
} else if ((e.stateMask & SWT.MODIFIER_MASK) == SWT.MOD1) {
// highlight (underline) word that would be used by MOD1 + mouse click
Point pt = screenPointToCell(e.x, e.y);
fCellCanvasModel.expandHoverSelectionAt(pt.y, pt.x);
} else {
fCellCanvasModel.expandHoverSelectionAt(-1, -1);
}
redraw();
});
serVerticalBarVisible(true);
setHorizontalBarVisible(false);
@ -540,4 +565,8 @@ public class TextCanvas extends GridCanvas {
fMouseListeners.remove(listener);
}
public String getHoverSelection() {
return fCellCanvasModel.getHoverSelectionText();
}
}

View file

@ -26,6 +26,8 @@ import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.RGB;
import org.eclipse.swt.widgets.Display;
import org.eclipse.tm.internal.terminal.control.impl.TerminalPlugin;
import org.eclipse.tm.internal.terminal.provisional.api.Logger;
import org.eclipse.tm.terminal.model.ITerminalTextDataReadOnly;
import org.eclipse.tm.terminal.model.LineSegment;
import org.eclipse.tm.terminal.model.TerminalColor;
@ -35,6 +37,7 @@ import org.eclipse.tm.terminal.model.TerminalStyle;
*
*/
public class TextLineRenderer implements ILinelRenderer {
private static final boolean DEBUG_HOVER = TerminalPlugin.isOptionEnabled(Logger.TRACE_DEBUG_LOG_HOVER);
private final ITextCanvasModel fModel;
private final StyleMap fStyleMap;
@ -76,6 +79,20 @@ public class TextLineRenderer implements ILinelRenderer {
drawText(doubleBufferGC, 0, 0, colFirst, segment.getColumn(), text);
drawCursor(model, doubleBufferGC, line, 0, 0, colFirst);
}
if (fModel.hasHoverSelection(line)) {
if (DEBUG_HOVER) {
System.out.format("hover: %s contains hover selection\n", line); //$NON-NLS-1$
}
Point hsStart = fModel.getHoverSelectionStart();
Point hsEnd = fModel.getHoverSelectionEnd();
int colStart = line == hsStart.y ? hsStart.x : 0;
int colEnd = line == hsEnd.y ? hsEnd.x : getTerminalText().getWidth();
if (colStart < colEnd) {
RGB defaultFg = fStyleMap.getForegrondRGB(null);
doubleBufferGC.setForeground(new Color(doubleBufferGC.getDevice(), defaultFg));
drawUnderline(doubleBufferGC, colStart, colEnd);
}
}
if (fModel.hasLineSelection(line)) {
TerminalStyle style = TerminalStyle.getStyle(TerminalColor.SELECTION_FOREGROUND,
TerminalColor.SELECTION_BACKGROUND);
@ -168,6 +185,21 @@ public class TextLineRenderer implements ILinelRenderer {
}
}
/**
*
* @param gc
* @param colStart Starting text column to underline (inclusive)
* @param colEnd Ending text column to underline (inclusive)
*/
private void drawUnderline(GC gc, int colStart, int colEnd) {
int y = getCellHeight() - 1;
int x = getCellWidth() * colStart;
// x2 is the right side of last column being underlined.
int x2 = (colEnd + 1) * getCellWidth() - 1;
gc.drawLine(x, y, x2, y);
}
private void setupGC(GC gc, TerminalStyle style) {
RGB foregrondColor = fStyleMap.getForegrondRGB(style);
gc.setForeground(new Color(gc.getDevice(), foregrondColor));

View file

@ -0,0 +1,96 @@
/*******************************************************************************
* Copyright (c) 2021 Fabrizio Iannetti.
*
* 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
*******************************************************************************/
package org.eclipse.tm.terminal.model;
import org.eclipse.swt.graphics.Point;
/**
* Represents a range of text in the terminal.
* <p>
* Used, for example, to store location of active hover
*
* @since 5.2
*/
public final class TextRange {
public final int colStart;
public final int colEnd;
public final int rowStart;
public final int rowEnd;
public final String text;
public static final TextRange EMPTY = new TextRange(0, 0, 0, 0, ""); //$NON-NLS-1$
/**
* Constructor.
*
* @param rowStart start row
* @param rowEnd end row
* @param colStart start column (exclusive)
* @param colEnd end column (exclusive)
* @param text text in the range
*/
public TextRange(int rowStart, int rowEnd, int colStart, int colEnd, String text) {
super();
this.colStart = colStart;
this.colEnd = colEnd;
this.rowStart = rowStart;
this.rowEnd = rowEnd;
this.text = text;
}
public boolean contains(int col, int row) {
int colStartInrow = row == rowStart ? colStart : 0;
int colEndInRow = row == rowEnd - 1 ? colEnd : col + 1;
return col >= colStartInrow && col < colEndInRow && row >= rowStart && row < rowEnd;
}
public boolean contains(int line) {
return line >= rowStart && line < rowEnd;
}
/**
* Whether the range represents a non-empty (non-zero) amount of text
*/
public boolean isEmpty() {
return !(colEnd > colStart || rowEnd > rowStart);
}
public Point getStart() {
return new Point(colStart, rowStart);
}
public Point getEnd() {
return new Point(colEnd, rowEnd);
}
public int getColStart() {
return colStart;
}
public int getColEnd() {
return colEnd;
}
public int getRowStart() {
return rowStart;
}
public int getRowEnd() {
return rowEnd;
}
@Override
public String toString() {
return String.format("TextRange (%s,%s)-(%s,%s)-'%s'", //$NON-NLS-1$
colStart, rowStart, colEnd, rowEnd, text);
}
}

View file

@ -2,7 +2,7 @@ Manifest-Version: 1.0
Bundle-ManifestVersion: 2
Bundle-Name: %pluginName
Bundle-SymbolicName: org.eclipse.tm.terminal.view.ui;singleton:=true
Bundle-Version: 4.8.0.qualifier
Bundle-Version: 4.8.100.qualifier
Bundle-Activator: org.eclipse.tm.terminal.view.ui.activator.UIPlugin
Bundle-Vendor: %providerName
Require-Bundle: org.eclipse.core.expressions;bundle-version="3.4.400",
@ -13,7 +13,10 @@ Require-Bundle: org.eclipse.core.expressions;bundle-version="3.4.400",
org.eclipse.egit.ui;bundle-version="2.0.0";resolution:=optional,
org.eclipse.tm.terminal.view.core;bundle-version="4.5.0",
org.eclipse.tm.terminal.control;bundle-version="4.5.0",
org.eclipse.ui;bundle-version="3.8.0"
org.eclipse.ui;bundle-version="3.8.0",
org.eclipse.ui.ide;bundle-version="3.18.0";resolution:=optional,
org.eclipse.ui.editors;bundle-version="3.14.0";resolution:=optional,
org.eclipse.text;bundle-version="3.11.0";resolution:=optional
Bundle-RequiredExecutionEnvironment: JavaSE-11
Bundle-ActivationPolicy: lazy
Bundle-Localization: plugin

View file

@ -16,6 +16,9 @@ import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Status;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.jface.resource.ImageRegistry;
import org.eclipse.swt.custom.CTabFolder;
@ -253,4 +256,21 @@ public class UIPlugin extends AbstractUIPlugin {
public static ImageDescriptor getImageDescriptor(String key) {
return getDefault().getImageRegistry().getDescriptor(key);
}
public static void log(String msg, Throwable e) {
log(new Status(IStatus.ERROR, getUniqueIdentifier(), IStatus.ERROR, msg, e));
}
public static void log(IStatus status) {
getDefault().getLog().log(status);
}
public static boolean isOptionEnabled(String strOption) {
String strEnabled = Platform.getDebugOption(strOption);
if (strEnabled == null)
return false;
return Boolean.parseBoolean(strEnabled);
}
}

View file

@ -0,0 +1,271 @@
/*******************************************************************************
* Copyright (c) 2021 Fabrizio Iannetti.
*
* 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
*******************************************************************************/
package org.eclipse.tm.terminal.view.ui.tabs;
import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.eclipse.core.commands.ExecutionException;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.Adapters;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Platform;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.window.Window;
import org.eclipse.swt.SWT;
import org.eclipse.tm.internal.terminal.control.ITerminalMouseListener2;
import org.eclipse.tm.internal.terminal.control.ITerminalViewControl;
import org.eclipse.tm.internal.terminal.provisional.api.Logger;
import org.eclipse.tm.terminal.model.ITerminalTextDataReadOnly;
import org.eclipse.tm.terminal.view.ui.activator.UIPlugin;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.IWorkbenchPartSite;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.PartInitException;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.ide.IDE;
import org.eclipse.ui.internal.ide.dialogs.OpenResourceDialog;
import org.eclipse.ui.texteditor.IDocumentProvider;
import org.eclipse.ui.texteditor.ITextEditor;
import org.osgi.framework.Bundle;
/**
* @noreference This class is not intended to be referenced by clients.
*/
public class OpenFileMouseHandler implements ITerminalMouseListener2 {
private static final boolean DEBUG_HOVER = UIPlugin.isOptionEnabled(Logger.TRACE_DEBUG_LOG_HOVER);
private static final List<String> NEEDED_BUNDLES = //
List.of("org.eclipse.core.resources", //$NON-NLS-1$
"org.eclipse.ui.ide", //$NON-NLS-1$
"org.eclipse.ui.editors", //$NON-NLS-1$
"org.eclipse.text"); //$NON-NLS-1$
private final ITerminalViewControl terminal;
private Pattern regex = Pattern.compile("(\\d*)(:(\\d*))?.*"); //$NON-NLS-1$
private IWorkbenchPartSite site;
/**
* Check if we have the bundles needed.
*/
private boolean neededBundlesAvailable;
OpenFileMouseHandler(IWorkbenchPartSite site, ITerminalViewControl terminal) {
this.site = site;
this.terminal = terminal;
neededBundlesAvailable = true;
for (String bundleName : NEEDED_BUNDLES) {
if (!bundleAvailable(bundleName)) {
this.neededBundlesAvailable = false;
if (DEBUG_HOVER) {
System.out.format(
"hover: the %s bundle is not present, therefore full ctrl-click functionality is not available\n", //$NON-NLS-1$
bundleName);
}
}
}
if (neededBundlesAvailable && DEBUG_HOVER) {
System.out.format("hover: the bundles needed for full ctrl-click functionality are available\n"); //$NON-NLS-1$
}
}
@Override
public void mouseUp(ITerminalTextDataReadOnly terminalText, int line, int column, int button, int stateMask) {
if ((stateMask & SWT.MODIFIER_MASK) != SWT.MOD1) {
// Only handle Ctrl-click
return;
}
String textToOpen = terminal.getHoverSelection();
String lineAndCol = null;
if (textToOpen.length() > 0) {
try {
// if the selection looks like a web URL, open using the browser
if (textToOpen.startsWith("http://") || textToOpen.startsWith("https://")) { //$NON-NLS-1$//$NON-NLS-2$
try {
PlatformUI.getWorkbench().getBrowserSupport().createBrowser(null).openURL(new URL(textToOpen));
return;
} catch (MalformedURLException e) {
// not a valid URL, continue
}
}
// After this we need Eclipse IDE features. If we don't have them then we stop here.
if (!neededBundlesAvailable) {
return;
}
// extract the path from file:// URLs
if (textToOpen.startsWith("file://")) { //$NON-NLS-1$
textToOpen = textToOpen.substring(7);
}
// remove optional position info name:[row[:col]]
{
int startOfRowCol = textToOpen.indexOf(':');
if (startOfRowCol == 1 && textToOpen.length() > 2) {
// assume this is the device separator on Windows
startOfRowCol = textToOpen.indexOf(':', startOfRowCol + 1);
}
if (startOfRowCol >= 0) {
lineAndCol = textToOpen.substring(startOfRowCol + 1);
textToOpen = textToOpen.substring(0, startOfRowCol);
}
}
Optional<String> fullPath = Optional.empty();
if (!textToOpen.startsWith("/")) { //$NON-NLS-1$
// relative path: try to append to the working directory
Optional<String> workingDirectory = terminal.getTerminalConnector().getWorkingDirectory();
if (workingDirectory.isPresent()) {
fullPath = Optional.of(workingDirectory.get() + "/" + textToOpen);
}
}
// if the selection is a file location that maps to a resource
// open the resource
IFile fileForLocation = ResourcesPlugin.getWorkspace().getRoot()
.getFileForLocation(new Path(fullPath.orElse(textToOpen)));
if (fileForLocation != null && fileForLocation.exists()) {
IEditorPart editor = IDE.openEditor(
PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage(), fileForLocation,
true);
goToLine(lineAndCol, editor);
return;
}
// try an external file, if it exists
File file = new File(fullPath.orElse(textToOpen));
if (file.exists() && !file.isDirectory()) {
try {
IEditorPart editor = IDE.openEditor(site.getPage(), file.toURI(),
IDE.getEditorDescriptor(file.getName(), true, true).getId(), true);
goToLine(lineAndCol, editor);
return;
} catch (Exception e) {
// continue
}
}
OpenResourceDialog openResourceDialog = new OpenResourceDialog(site.getShell(),
ResourcesPlugin.getPlugin().getWorkspace().getRoot(), IResource.FILE);
openResourceDialog.setInitialPattern(textToOpen);
if (openResourceDialog.open() != Window.OK)
return;
Object[] results = openResourceDialog.getResult();
List<IFile> files = new ArrayList<>();
for (Object result : results) {
if (result instanceof IFile) {
files.add((IFile) result);
}
}
if (files.size() > 0) {
final IWorkbenchWindow window = PlatformUI.getWorkbench().getActiveWorkbenchWindow();
if (window == null) {
throw new ExecutionException("no active workbench window"); //$NON-NLS-1$
}
final IWorkbenchPage page = window.getActivePage();
if (page == null) {
throw new ExecutionException("no active workbench page"); //$NON-NLS-1$
}
try {
for (IFile iFile : files) {
IEditorPart editor = IDE.openEditor(page, iFile, true);
goToLine(lineAndCol, editor);
}
} catch (final PartInitException e) {
throw new ExecutionException("error opening file in editor", e); //$NON-NLS-1$
}
}
} catch (IllegalArgumentException | NullPointerException | ExecutionException | PartInitException e) {
UIPlugin.log("Failed to activate OpenResourceDialog", e); //$NON-NLS-1$
}
}
}
private boolean bundleAvailable(String symbolicName) {
Bundle bundle = Platform.getBundle(symbolicName);
boolean available = bundle != null && bundle.getState() != Bundle.UNINSTALLED
&& bundle.getState() != Bundle.STOPPING;
return available;
}
private void goToLine(String lineAndCol, IEditorPart editor) {
ITextEditor textEditor = Adapters.adapt(editor, ITextEditor.class);
if (textEditor != null) {
Optional<Integer> optionalOffset = getRegionFromLineAndCol(textEditor, lineAndCol);
optionalOffset.ifPresent(offset -> textEditor.selectAndReveal(offset, 0));
}
}
/**
* Returns the line information for the given line in the given editor
*/
private Optional<Integer> getRegionFromLineAndCol(ITextEditor editor, String lineAndCol) {
if (lineAndCol == null) {
return Optional.empty();
}
Matcher matcher = regex.matcher(lineAndCol);
if (!matcher.matches()) {
return Optional.empty();
}
String lineStr = matcher.group(1);
String colStr = matcher.group(3);
int line;
int col = 0;
try {
line = Integer.parseInt(lineStr);
} catch (NumberFormatException e1) {
return Optional.empty();
}
try {
col = Integer.parseInt(colStr);
} catch (NumberFormatException e1) {
// if we can't get a column, go to the line alone
}
IDocumentProvider provider = editor.getDocumentProvider();
IEditorInput input = editor.getEditorInput();
try {
provider.connect(input);
} catch (CoreException e) {
return null;
}
try {
IDocument document = provider.getDocument(input);
if (document != null && line > 0) {
// document's lines are 0-offset
line = line - 1;
int lineOffset = document.getLineOffset(line);
if (col > 0) {
int lineLength = document.getLineLength(line);
if (col < lineLength) {
lineOffset += col;
}
}
return Optional.of(lineOffset);
}
} catch (BadLocationException e) {
} finally {
provider.disconnect(input);
}
return Optional.empty();
}
}

View file

@ -271,6 +271,10 @@ public class TabFolderManager extends PlatformObject implements ISelectionProvid
// Add middle mouse button paste support
addMiddleMouseButtonPasteSupport(terminal);
// add support to open resource on ctrl/meta + mouse click
addOpenResourceSupport(terminal);
// Add the "selection" listener to the terminal control
new TerminalControlSelectionListener(terminal);
// Configure the terminal encoding
@ -328,6 +332,10 @@ public class TabFolderManager extends PlatformObject implements ISelectionProvid
return item;
}
private void addOpenResourceSupport(ITerminalViewControl terminal) {
terminal.addMouseListener(new OpenFileMouseHandler(getParentView().getSite(), terminal));
}
/**
* Used for DnD of terminal tab items between terminal views
* <p>