Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

open file when text is hovered #179

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions dependencies.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"windows": {
">3000": [
"psutil",
"pyte",
"pywinpty",
"wcwidth"
Expand All @@ -9,6 +10,7 @@

"*": {
">3000": [
"psutil",
"ptyprocess",
"pyte",
"wcwidth"
Expand Down
151 changes: 101 additions & 50 deletions terminus/mouse.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
import sublime
import sublime_plugin

import os
import re
import logging
import webbrowser

from .terminal import Terminal, CONTINUATION
from .utils import highlight_key
from .utils import highlight_key, istext

logger = logging.getLogger('Terminus')

rex = re.compile(
URL_REGEX = re.compile(
r'''(?x)
\b(?:
https?://(?:(?:[a-zA-Z0-9\-_]+(?:\.[a-zA-Z0-9\-._]+)+)|localhost)| # http://
Expand All @@ -20,7 +21,31 @@
[a-zA-Z0-9\-_~:/#@$*+=] # allowed end chars
''')

URL_POPUP = """

FILE_REGEX = re.compile(r'''
(?x)
(?:\b|\/)(?:[a-zA-Z]:[\\/])? # windows drive or unix root
(?:\\.|[^ ?%*:|"<>\n])+ # escaped space or not reserved symbols
(?:\.(?:[^ ?%*:|"<>.\n])+) # must have extensions
(?::[0-9]+){0,2}
|
(?<=") # begin double quotation
(?:\b|\/)(?:[a-zA-Z]:[\\/])? # windows drive or unix root
(?:[^?%*:|"<>\n])+ # not reserved symbols
(?:\.(?:[^ ?%*:|"<>.\n])+) # must have extensions
(?::[0-9]+){0,2}
(?=") # end double quotation
|
(?<=') # begin single quotation
(?:\b|\/)(?:[a-zA-Z]:[\\/])? # windows drive or unix root
(?:[^?%*:|"<>\n])+ # not reserved symbols
(?:\.(?:[^ ?%*:|"<>.\n])+) # must have extensions
(?::[0-9]+){0,2}
(?=') # end single quotation
''')


POPUP = """
<style>
body {
margin: 0px;
Expand All @@ -41,30 +66,7 @@
"""


def find_url(view, event=None, pt=None):
if event:
pt = view.window_to_text((event["x"], event["y"]))
line = view.line(pt)

line.a = max(line.a, pt - 1024)
line.b = pt + 1024

text = view.substr(line)
text = text.replace(CONTINUATION + "\n", "")
it = rex.finditer(text)

for match in it:
if match.start() <= (pt - line.a) and match.end() >= (pt - line.a):
url = text[match.start():match.end()]
if url[0:3] == "www":
return "http://" + url
else:
return url

return None


def find_url_region(view, event=None, pt=None):
def find_regex(rex, view, event=None, pt=None):
if event:
pt = view.window_to_text((event["x"], event["y"]))
line = view.line(pt)
Expand All @@ -78,13 +80,34 @@ def find_url_region(view, event=None, pt=None):

for match in rex.finditer(text):
if match.start() <= (pt - line.a) and match.end() >= (pt - line.a):
thing = text[match.start():match.end()]
a = match.start()
b = match.end()
for marker in re.finditer(CONTINUATION + "\n", original_text):
if a <= marker.start() and b >= marker.start():
b += len(CONTINUATION) + 1
return (line.a + a, line.a + b)
return None
return thing, (line.a + a, line.a + b)
return None, None


def find_url(view, event=None, pt=None):
url, region = find_regex(URL_REGEX, view, event, pt)
if url and url[0:3] == "www":
url = "http://" + url
return url, region


def find_file(view, event=None, pt=None):
# TODO: allow user file_regex
path, region = find_regex(FILE_REGEX, view, event, pt)
if not path:
return None, None, None, None, None
path = path.replace("\\ ", " ").replace("\\(", "(").replace("\\)", ")")
m = re.match(r"^(.*?)(?::([0-9]+))?(?::([0-9]+))?$", path)
if not m:
return None, None, None, None, None
path, row, col = m.groups()
return path, row or '', col or '', "", region


class TerminusMouseEventListener(sublime_plugin.EventListener):
Expand All @@ -103,39 +126,67 @@ def on_hover(self, view, point, hover_zone):
return
if hover_zone != sublime.HOVER_TEXT:
return
url = find_url(view, pt=point)
url, region = find_url(view, pt=point)

if not url:
return
if url:
hover = "url"
else:
hover = "file"
path, row, col, _, region = find_file(view, pt=point)
if not path:
return
if os.path.isabs(path):
if not os.path.exists(path):
return
else:
try:
cwd = terminal.cwd()
except Exception:
cwd = view.settings().get("terminus_view.args").get("cwd")
path = os.path.join(cwd, path)
if not os.path.exists(path):
return

def on_navigate(action):
if action == "open":
webbrowser.open_new_tab(url)
if hover == "url":
webbrowser.open_new_tab(url)
elif hover == "file":
if not os.path.isfile(path):
return
if istext(path):
view.window().run_command(
"open_file",
{
"file": path + row + col,
"flags": sublime.ENCODED_POSITION | sublime.TRANSIENT
}
)
else:
view.window().run_command(
"open_dir",
{"dir": os.path.dirname(path), "file": os.path.basename(path)})

def on_hide():
if link_key:
view.erase_regions(link_key)

url_region = find_url_region(view, pt=point)
link_key = None
if url_region:
link_key = highlight_key(view)
view.add_regions(
link_key,
[sublime.Region(*url_region)],
"meta",
flags=sublime.DRAW_NO_FILL | sublime.DRAW_NO_OUTLINE | sublime.DRAW_SOLID_UNDERLINE)
view.erase_regions(link_key)

link_key = highlight_key(view)
view.add_regions(
link_key,
[sublime.Region(*region)],
"meta",
flags=sublime.DRAW_NO_FILL | sublime.DRAW_NO_OUTLINE | sublime.DRAW_SOLID_UNDERLINE)

view.show_popup(
URL_POPUP,
POPUP,
sublime.HIDE_ON_MOUSE_MOVE_AWAY,
location=point,
on_navigate=on_navigate, on_hide=on_hide)


class TerminusOpenContextUrlCommand(sublime_plugin.TextCommand):
def run(self, edit, event):
url = find_url(self.view, event)
url = find_url(self.view, event)[0]
webbrowser.open_new_tab(url)

def is_enable(self, *args, **kwargs):
Expand All @@ -144,13 +195,13 @@ def is_enable(self, *args, **kwargs):

def is_visible(self, event):
terminal = Terminal.from_id(self.view.id())
return terminal is not None and find_url(self.view, event) is not None
return terminal is not None and find_url(self.view, event)[0] is not None

def description(self, event):
url = find_url(self.view, event)
url = find_url(self.view, event)[0]
if len(url) > 64:
url = url[0:64] + "..."
return "Open " + url
return "Terminus Open " + url

def want_event(self):
return True
Expand Down
4 changes: 4 additions & 0 deletions terminus/terminal.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import logging
import tempfile
import threading
import psutil
from queue import Queue, Empty

from .ptty import TerminalPtyProcess, TerminalScreen, TerminalStream
Expand Down Expand Up @@ -69,6 +70,9 @@ def cull_terminals(cls):
for terminal in terminals_to_close:
terminal.close()

def cwd(self):
return psutil.Process(self.process.pid).cwd()

def attach_view(self, view, offset=None):
with self.lock:
self.view = view
Expand Down
6 changes: 6 additions & 0 deletions terminus/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,3 +115,9 @@ def _(on_change):
settings.add_on_change(key, on_change_factory(key, on_change))

return _


def istext(filename):
s = open(filename, 'rb').read(512)
t = s.decode("utf-8", "ignore").encode()
return len(t) == len(s)