Skip to content

Commit

Permalink
extendable tooltip renderer
Browse files Browse the repository at this point in the history
  • Loading branch information
srusskih committed May 30, 2017
1 parent 2b01cc2 commit b954351
Show file tree
Hide file tree
Showing 7 changed files with 205 additions and 131 deletions.
4 changes: 2 additions & 2 deletions .gitignore
@@ -1,6 +1,6 @@
jedic
*.py[co]
*.sublime
*.sublime-project
*.sublime-workspace
*.ropeproject
package-metadata.json
.DS_Store
Expand Down
6 changes: 0 additions & 6 deletions __init__.py
@@ -1,9 +1,3 @@
# fix absolute imports on ST3
# TODO: remove
#import sys
#import os
#sys.path.insert(0, os.path.abspath(os.path.dirname(__file__)))

try:
from sublime_jedi import *
except ImportError:
Expand Down
145 changes: 22 additions & 123 deletions sublime_jedi/helper.py
@@ -1,24 +1,15 @@
# -*- coding: utf-8 -*-
from functools import partial

import sublime
import sublime_plugin

import html

try:
# mdpopups needs 3119+ for wrapper_class, which diff popup relies on
if int(sublime.version()) < 3119:
raise ImportError('Sublime Text 3119+ required.')
# mdpopups 1.9.0+ is required because of wrapper_class and templates
import mdpopups
if mdpopups.version() < (1, 9, 0):
raise ImportError('mdpopups 1.9.0+ required.')
_HAVE_MDPOPUPS = True
except ImportError:
_HAVE_MDPOPUPS = False

from .console_logging import getLogger
from .settings import get_plugin_settings
from .utils import ask_daemon, PythonCommandMixin, is_sublime_v2, is_python_scope
from .tooltips import show_docstring_tooltip
from .utils import (
ask_daemon, is_python_scope, is_sublime_v2, PythonCommandMixin
)

logger = getLogger(__name__)

Expand All @@ -29,7 +20,7 @@ def run(self, edit, docstring):
self.view.insert(edit, self.view.size(), docstring)


def docstring_panel(view, docstring):
def show_docstring_panel(view, docstring):
"""Show docstring in output panel.
:param view (sublime.View): current active view
Expand All @@ -48,111 +39,21 @@ def docstring_panel(view, docstring):
sublime.status_message('Jedi: No results!')


def docstring_tooltip(view, docstring, location=None):
"""Show docstring in popup.
:param view (sublime.View): current active view
:param docstring (basestring): python __doc__ string
:param location (int): The text point where to create the popup
"""
if not docstring:
return sublime.status_message('Jedi: No results!')

if location is None:
location = view.sel()[0].begin()

# fallback if mdpopups is not available
if not _HAVE_MDPOPUPS:
content = simple_html_builder(docstring)
return view.show_popup(content, location=location, max_width=512)

# Some basic defaults, which can be modified by user in the
# Packages/User/mdpopups.css file.
css = """
body {
margin: 6px;
}
div.mdpopups {
margin: 0;
padding: 0;
}
.jedi h6 {
font-weight: bold;
color: var(--bluish);
}
"""
return mdpopups.show_popup(
view=view,
content=markdown_html_builder(view, docstring),
location=location,
max_width=800,
md=True,
css=css,
wrapper_class='jedi',
flags=sublime.HIDE_ON_MOUSE_MOVE_AWAY)


def markdown_html_builder(view, docstring):
doclines = docstring.split('\n')
signature = doclines[0].strip()
# first line is a signature if it contains parentheses
if '(' in signature:

def is_class(string):
"""Check whether string contains a class or function signature."""
for c in string:
if c != '_':
break
return c.isupper()

# a hackish way to determine whether it is a class or function
prefix = 'class' if is_class(signature) else 'def'
# highlight signature
content = '```python\n{0} {1}\n```\n'.format(prefix, signature)
# merge the rest of the docstring beginning with 3rd line
# skip leading and tailing empty lines
docstring = '\n'.join(doclines[1:]).strip()
content += html.escape(docstring, quote=False)
else:
# docstring does not contain signature
content = html.escape(docstring, quote=False)

# preserve empty lines
content = content.replace('\n\n', '\n\u00A0\n')
# preserve whitespace
content = content.replace(' ', '\u00A0\u00A0')
# convert markdown to html
content = mdpopups.md2html(view, content)
# highlight headlines ( Google Python Style Guide )
keywords = (
'Args:', 'Arguments:', 'Attributes:', 'Example:', 'Examples:', 'Note:',
'Raises:', 'Returns:', 'Yields:')
for keyword in keywords:
content = content.replace(
keyword + '<br />', '<h6>' + keyword + '</h6>')
return content


def simple_html_builder(docstring):
docstring = html.escape(docstring, quote=False).split('\n')
docstring[0] = '<b>' + docstring[0] + '</b>'
content = '<body><p style="font-family: sans-serif;">{0}</p></body>'.format(
'<br />'.join(docstring)
)
return content


class SublimeJediDocstring(PythonCommandMixin, sublime_plugin.TextCommand):
"""Show docstring."""

def run(self, edit):
ask_daemon(self.view, self.render, 'docstring')

def render(self, view, docstring):
if docstring is None or docstring == '':
logger.debug('Empty docstring.')
return

if is_sublime_v2():
docstring_panel(view, docstring)
show_docstring_panel(view, docstring)
else:
docstring_tooltip(view, docstring)
show_docstring_tooltip(view, docstring)


class SublimeJediSignature(PythonCommandMixin, sublime_plugin.TextCommand):
Expand All @@ -175,22 +76,20 @@ def enabled(self):

def on_activated(self, view):
"""Handle view.on_activated event."""
if not self.enabled():
return
if not view.match_selector(0, 'source.python'):
if not (self.enabled() and view.match_selector(0, 'source.python')):
return

# disable default goto definition popup
view.settings().set('show_definitions', False)

def on_hover(self, view, point, hover_zone):
"""Handle view.on_hover event."""
if hover_zone != sublime.HOVER_TEXT:
return
if not self.enabled():
return
if not is_python_scope(view, point):
if not (hover_zone == sublime.HOVER_TEXT
and self.enabled()
and is_python_scope(view, point)):
return

def render(view, docstring):
docstring_tooltip(view, docstring, point)
ask_daemon(view, render, 'docstring', point)
ask_daemon(view,
partial(show_docstring_tooltip, location=point),
'docstring',
point)
29 changes: 29 additions & 0 deletions sublime_jedi/tooltips/__init__.py
@@ -0,0 +1,29 @@
# -*- coding: utf-8 -*-

from .markdown import MarkDownTooltip
from .simple import SimpleTooltip


def _guess_docstring_format(docstring):
"""Find proper tooltip class for docstring.
Docstrings could has different format, and we should pick a proper
tooltip for it.
:rtype: sublime_jedi.tooltips.base.Tooltip
"""
for tooltip_class in [MarkDownTooltip]:
return tooltip_class.guess(docstring) and tooltip_class()

return SimpleTooltip()


def show_docstring_tooltip(view, docstring, location=None):
"""Show docstring in popup.
:param view (sublime.View): current active view
:param docstring (basestring): python __doc__ string
:param location (int): The text point where to create the popup
"""
tooltip = _guess_docstring_format(docstring)
tooltip.show_popup(view, docstring, location)
22 changes: 22 additions & 0 deletions sublime_jedi/tooltips/base.py
@@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
import abc


class Tooltip:

__metaclass__ = abc.ABCMeta # works for 2.7 and 3+

@classmethod
@abc.abstractmethod
def guess(cls, docstring):
"""Check if tooltip can render the docstring.
:rtype: bool
"""

@abc.abstractmethod
def show_popup(self, view, docstring, location=None):
"""Show tooltip with docstring.
:rtype: NoneType
"""
104 changes: 104 additions & 0 deletions sublime_jedi/tooltips/markdown.py
@@ -0,0 +1,104 @@
# -*- coding: utf-8 -*-
import html

import sublime

try:
# mdpopups needs 3119+ for wrapper_class, which diff popup relies on
if int(sublime.version()) < 3119:
raise ImportError('Sublime Text 3119+ required.')

import mdpopups

# mdpopups 1.9.0+ is required because of wrapper_class and templates
if mdpopups.version() < (1, 9, 0):
raise ImportError('mdpopups 1.9.0+ required.')

_HAVE_MDPOPUPS = True
except ImportError:
_HAVE_MDPOPUPS = False

from .base import Tooltip


class MarkDownTooltip(Tooltip):

@classmethod
def guess(cls, docstring):
return _HAVE_MDPOPUPS

def _get_style(self):
css = """
body {
margin: 6px;
}
div.mdpopups {
margin: 0;
padding: 0;
}
.jedi h6 {
font-weight: bold;
color: var(--bluish);
}
"""
return css

def _build_html(self, view, docstring):
""" Convert python docstring to text ready to show in popup.
:param view: sublime text view object
:param docstring: python docstring as a string
"""
doclines = docstring.split('\n')
signature = doclines[0].strip()
# first line is a signature if it contains parentheses
if '(' in signature:

def is_class(string):
"""Check whether string contains a class or function signature."""
for c in string:
if c != '_':
break
return c.isupper()

# a hackish way to determine whether it is a class or function
prefix = 'class' if is_class(signature) else 'def'
# highlight signature
content = '```python\n{0} {1}\n```\n'.format(prefix, signature)
# merge the rest of the docstring beginning with 3rd line
# skip leading and tailing empty lines
docstring = '\n'.join(doclines[1:]).strip()
content += html.escape(docstring, quote=False)
else:
# docstring does not contain signature
content = html.escape(docstring, quote=False)

# preserve empty lines
content = content.replace('\n\n', '\n\u00A0\n')
# preserve whitespace
content = content.replace(' ', '\u00A0\u00A0')
# convert markdown to html
content = mdpopups.md2html(view, content)
# highlight headlines ( Google Python Style Guide )
keywords = (
'Args:', 'Arguments:', 'Attributes:', 'Example:', 'Examples:', 'Note:',
'Raises:', 'Returns:', 'Yields:')
for keyword in keywords:
content = content.replace(
keyword + '<br />', '<h6>' + keyword + '</h6>')
return content

def show_popup(self, view, docstring, location=None):
if location is None:
location = view.sel()[0].begin()

mdpopups.show_popup(
view=view,
content=self._build_html(view, docstring),
location=location,
max_width=800,
md=True,
css=self._get_style(),
wrapper_class='jedi',
flags=sublime.HIDE_ON_MOUSE_MOVE_AWAY)

26 changes: 26 additions & 0 deletions sublime_jedi/tooltips/simple.py
@@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
import html

from .base import Tooltip


class SimpleTooltip(Tooltip):

@classmethod
def guess(cls, docstring):
return True

def _build_html(self, docstring):
docstring = html.escape(docstring, quote=False).split('\n')
docstring[0] = '<b>' + docstring[0] + '</b>'
content = '<body><p style="font-family: sans-serif;">{0}</p></body>'.format(
'<br />'.join(docstring)
)
return content

def show_popup(self, view, docstring, location=None):
if location is None:
location = view.sel()[0].begin()

content = self._build_html(docstring)
view.show_popup(content, location=location, max_width=512)

0 comments on commit b954351

Please sign in to comment.