annotations: replace annotated source code viewer with renderer
to allow soft wrapping of lines and viewing of annotation message
This commit is contained in:
parent
323c951802
commit
8f4e395877
11 changed files with 546 additions and 372 deletions
|
|
@ -36,6 +36,8 @@ from webob.exc import HTTPNotFound, HTTPBadRequest
|
|||
from rhodecode.controllers.utils import parse_path_ref
|
||||
from rhodecode.lib import diffs, helpers as h, caches
|
||||
from rhodecode.lib.compat import OrderedDict
|
||||
from rhodecode.lib.codeblocks import (
|
||||
filenode_as_lines_tokens, filenode_as_annotated_lines_tokens)
|
||||
from rhodecode.lib.utils import jsonify, action_logger
|
||||
from rhodecode.lib.utils2 import (
|
||||
convert_line_endings, detect_mode, safe_str, str2bool)
|
||||
|
|
@ -221,9 +223,15 @@ class FilesController(BaseRepoController):
|
|||
c.file_author = True
|
||||
c.file_tree = ''
|
||||
if c.file.is_file():
|
||||
c.renderer = (
|
||||
c.renderer and h.renderer_from_filename(c.file.path))
|
||||
c.file_last_commit = c.file.last_commit
|
||||
if c.annotate: # annotation has precedence over renderer
|
||||
c.annotated_lines = filenode_as_annotated_lines_tokens(
|
||||
c.file)
|
||||
else:
|
||||
c.renderer = (
|
||||
c.renderer and h.renderer_from_filename(c.file.path))
|
||||
if not c.renderer:
|
||||
c.lines = filenode_as_lines_tokens(c.file)
|
||||
|
||||
c.on_branch_head = self._is_valid_head(
|
||||
commit_id, c.rhodecode_repo)
|
||||
|
|
|
|||
|
|
@ -1,214 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (C) 2011-2016 RhodeCode GmbH
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License, version 3
|
||||
# (only), as published by the Free Software Foundation.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
# This program is dual-licensed. If you wish to learn more about the
|
||||
# RhodeCode Enterprise Edition, including its added features, Support services,
|
||||
# and proprietary license terms, please see https://rhodecode.com/licenses/
|
||||
|
||||
|
||||
"""
|
||||
Anontation library for usage in rhodecode, previously part of vcs
|
||||
"""
|
||||
|
||||
import StringIO
|
||||
|
||||
from pygments import highlight
|
||||
from pygments.formatters import HtmlFormatter
|
||||
|
||||
from rhodecode.lib.vcs.exceptions import VCSError
|
||||
from rhodecode.lib.vcs.nodes import FileNode
|
||||
|
||||
|
||||
def annotate_highlight(
|
||||
filenode, annotate_from_commit_func=None,
|
||||
order=None, headers=None, **options):
|
||||
"""
|
||||
Returns html portion containing annotated table with 3 columns: line
|
||||
numbers, commit information and pygmentized line of code.
|
||||
|
||||
:param filenode: FileNode object
|
||||
:param annotate_from_commit_func: function taking commit and
|
||||
returning single annotate cell; needs break line at the end
|
||||
:param order: ordered sequence of ``ls`` (line numbers column),
|
||||
``annotate`` (annotate column), ``code`` (code column); Default is
|
||||
``['ls', 'annotate', 'code']``
|
||||
:param headers: dictionary with headers (keys are whats in ``order``
|
||||
parameter)
|
||||
"""
|
||||
from rhodecode.lib.helpers import get_lexer_for_filenode
|
||||
options['linenos'] = True
|
||||
formatter = AnnotateHtmlFormatter(
|
||||
filenode=filenode, order=order, headers=headers,
|
||||
annotate_from_commit_func=annotate_from_commit_func, **options)
|
||||
lexer = get_lexer_for_filenode(filenode)
|
||||
highlighted = highlight(filenode.content, lexer, formatter)
|
||||
return highlighted
|
||||
|
||||
|
||||
class AnnotateHtmlFormatter(HtmlFormatter):
|
||||
|
||||
def __init__(
|
||||
self, filenode, annotate_from_commit_func=None,
|
||||
order=None, **options):
|
||||
"""
|
||||
If ``annotate_from_commit_func`` is passed, it should be a function
|
||||
which returns string from the given commit. For example, we may pass
|
||||
following function as ``annotate_from_commit_func``::
|
||||
|
||||
def commit_to_anchor(commit):
|
||||
return '<a href="/commits/%s/">%s</a>\n' %\
|
||||
(commit.id, commit.id)
|
||||
|
||||
:param annotate_from_commit_func: see above
|
||||
:param order: (default: ``['ls', 'annotate', 'code']``); order of
|
||||
columns;
|
||||
:param options: standard pygment's HtmlFormatter options, there is
|
||||
extra option tough, ``headers``. For instance we can pass::
|
||||
|
||||
formatter = AnnotateHtmlFormatter(filenode, headers={
|
||||
'ls': '#',
|
||||
'annotate': 'Annotate',
|
||||
'code': 'Code',
|
||||
})
|
||||
|
||||
"""
|
||||
super(AnnotateHtmlFormatter, self).__init__(**options)
|
||||
self.annotate_from_commit_func = annotate_from_commit_func
|
||||
self.order = order or ('ls', 'annotate', 'code')
|
||||
headers = options.pop('headers', None)
|
||||
if headers and not (
|
||||
'ls' in headers and 'annotate' in headers and 'code' in headers):
|
||||
raise ValueError(
|
||||
"If headers option dict is specified it must "
|
||||
"all 'ls', 'annotate' and 'code' keys")
|
||||
self.headers = headers
|
||||
if isinstance(filenode, FileNode):
|
||||
self.filenode = filenode
|
||||
else:
|
||||
raise VCSError(
|
||||
"This formatter expect FileNode parameter, not %r" %
|
||||
type(filenode))
|
||||
|
||||
def annotate_from_commit(self, commit):
|
||||
"""
|
||||
Returns full html line for single commit per annotated line.
|
||||
"""
|
||||
if self.annotate_from_commit_func:
|
||||
return self.annotate_from_commit_func(commit)
|
||||
else:
|
||||
return commit.id + '\n'
|
||||
|
||||
def _wrap_tablelinenos(self, inner):
|
||||
dummyoutfile = StringIO.StringIO()
|
||||
lncount = 0
|
||||
for t, line in inner:
|
||||
if t:
|
||||
lncount += 1
|
||||
dummyoutfile.write(line)
|
||||
|
||||
fl = self.linenostart
|
||||
mw = len(str(lncount + fl - 1))
|
||||
sp = self.linenospecial
|
||||
st = self.linenostep
|
||||
la = self.lineanchors
|
||||
aln = self.anchorlinenos
|
||||
if sp:
|
||||
lines = []
|
||||
|
||||
for i in range(fl, fl + lncount):
|
||||
if i % st == 0:
|
||||
if i % sp == 0:
|
||||
if aln:
|
||||
lines.append('<a href="#%s-%d" class="special">'
|
||||
'%*d</a>' %
|
||||
(la, i, mw, i))
|
||||
else:
|
||||
lines.append('<span class="special">'
|
||||
'%*d</span>' % (mw, i))
|
||||
else:
|
||||
if aln:
|
||||
lines.append('<a href="#%s-%d">'
|
||||
'%*d</a>' % (la, i, mw, i))
|
||||
else:
|
||||
lines.append('%*d' % (mw, i))
|
||||
else:
|
||||
lines.append('')
|
||||
ls = '\n'.join(lines)
|
||||
else:
|
||||
lines = []
|
||||
for i in range(fl, fl + lncount):
|
||||
if i % st == 0:
|
||||
if aln:
|
||||
lines.append('<a href="#%s-%d">%*d</a>' \
|
||||
% (la, i, mw, i))
|
||||
else:
|
||||
lines.append('%*d' % (mw, i))
|
||||
else:
|
||||
lines.append('')
|
||||
ls = '\n'.join(lines)
|
||||
|
||||
cached = {}
|
||||
annotate = []
|
||||
for el in self.filenode.annotate:
|
||||
commit_id = el[1]
|
||||
if commit_id in cached:
|
||||
result = cached[commit_id]
|
||||
else:
|
||||
commit = el[2]()
|
||||
result = self.annotate_from_commit(commit)
|
||||
cached[commit_id] = result
|
||||
annotate.append(result)
|
||||
|
||||
annotate = ''.join(annotate)
|
||||
|
||||
# in case you wonder about the seemingly redundant <div> here:
|
||||
# since the content in the other cell also is wrapped in a div,
|
||||
# some browsers in some configurations seem to mess up the formatting.
|
||||
'''
|
||||
yield 0, ('<table class="%stable">' % self.cssclass +
|
||||
'<tr><td class="linenos"><div class="linenodiv"><pre>' +
|
||||
ls + '</pre></div></td>' +
|
||||
'<td class="code">')
|
||||
yield 0, dummyoutfile.getvalue()
|
||||
yield 0, '</td></tr></table>'
|
||||
|
||||
'''
|
||||
headers_row = []
|
||||
if self.headers:
|
||||
headers_row = ['<tr class="annotate-header">']
|
||||
for key in self.order:
|
||||
td = ''.join(('<td>', self.headers[key], '</td>'))
|
||||
headers_row.append(td)
|
||||
headers_row.append('</tr>')
|
||||
|
||||
body_row_start = ['<tr>']
|
||||
for key in self.order:
|
||||
if key == 'ls':
|
||||
body_row_start.append(
|
||||
'<td class="linenos"><div class="linenodiv"><pre>' +
|
||||
ls + '</pre></div></td>')
|
||||
elif key == 'annotate':
|
||||
body_row_start.append(
|
||||
'<td class="annotate"><div class="annotatediv"><pre>' +
|
||||
annotate + '</pre></div></td>')
|
||||
elif key == 'code':
|
||||
body_row_start.append('<td class="code">')
|
||||
yield 0, ('<table class="%stable">' % self.cssclass +
|
||||
''.join(headers_row) +
|
||||
''.join(body_row_start)
|
||||
)
|
||||
yield 0, dummyoutfile.getvalue()
|
||||
yield 0, '</td></tr></table>'
|
||||
147
rhodecode/lib/codeblocks.py
Normal file
147
rhodecode/lib/codeblocks.py
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (C) 2011-2016 RhodeCode GmbH
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License, version 3
|
||||
# (only), as published by the Free Software Foundation.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
# This program is dual-licensed. If you wish to learn more about the
|
||||
# RhodeCode Enterprise Edition, including its added features, Support services,
|
||||
# and proprietary license terms, please see https://rhodecode.com/licenses/
|
||||
|
||||
|
||||
from itertools import groupby
|
||||
|
||||
from pygments import lex
|
||||
# PYGMENTS_TOKEN_TYPES is used in a hot loop keep attribute lookups to a minimum
|
||||
from pygments.token import STANDARD_TYPES as PYGMENTS_TOKEN_TYPES
|
||||
|
||||
from rhodecode.lib.helpers import get_lexer_for_filenode
|
||||
|
||||
def tokenize_file(content, lexer):
|
||||
"""
|
||||
Use pygments to tokenize some content based on a lexer
|
||||
ensuring all original new lines and whitespace is preserved
|
||||
"""
|
||||
|
||||
lexer.stripall = False
|
||||
lexer.stripnl = False
|
||||
lexer.ensurenl = False
|
||||
return lex(content, lexer)
|
||||
|
||||
|
||||
def pygment_token_class(token_type):
|
||||
""" Convert a pygments token type to html class name """
|
||||
|
||||
fname = PYGMENTS_TOKEN_TYPES.get(token_type)
|
||||
if fname:
|
||||
return fname
|
||||
|
||||
aname = ''
|
||||
while fname is None:
|
||||
aname = '-' + token_type[-1] + aname
|
||||
token_type = token_type.parent
|
||||
fname = PYGMENTS_TOKEN_TYPES.get(token_type)
|
||||
|
||||
return fname + aname
|
||||
|
||||
|
||||
def tokens_as_lines(tokens, split_string=u'\n'):
|
||||
"""
|
||||
Take a list of (TokenType, text) tuples and split them by a string
|
||||
|
||||
eg. [(TEXT, 'some\ntext')] => [(TEXT, 'some'), (TEXT, 'text')]
|
||||
"""
|
||||
|
||||
buffer = []
|
||||
for token_type, token_text in tokens:
|
||||
parts = token_text.split(split_string)
|
||||
for part in parts[:-1]:
|
||||
buffer.append((token_type, part))
|
||||
yield buffer
|
||||
buffer = []
|
||||
|
||||
buffer.append((token_type, parts[-1]))
|
||||
|
||||
if buffer:
|
||||
yield buffer
|
||||
|
||||
|
||||
def filenode_as_lines_tokens(filenode):
|
||||
"""
|
||||
Return a generator of lines with pygment tokens for a filenode eg:
|
||||
|
||||
[
|
||||
(1, line1_tokens_list),
|
||||
(2, line1_tokens_list]),
|
||||
]
|
||||
"""
|
||||
|
||||
return enumerate(
|
||||
tokens_as_lines(
|
||||
tokenize_file(
|
||||
filenode.content, get_lexer_for_filenode(filenode)
|
||||
)
|
||||
),
|
||||
1)
|
||||
|
||||
|
||||
def filenode_as_annotated_lines_tokens(filenode):
|
||||
"""
|
||||
Take a file node and return a list of annotations => lines, if no annotation
|
||||
is found, it will be None.
|
||||
|
||||
eg:
|
||||
|
||||
[
|
||||
(annotation1, [
|
||||
(1, line1_tokens_list),
|
||||
(2, line2_tokens_list),
|
||||
]),
|
||||
(annotation2, [
|
||||
(3, line1_tokens_list),
|
||||
]),
|
||||
(None, [
|
||||
(4, line1_tokens_list),
|
||||
]),
|
||||
(annotation1, [
|
||||
(5, line1_tokens_list),
|
||||
(6, line2_tokens_list),
|
||||
])
|
||||
]
|
||||
"""
|
||||
|
||||
|
||||
# cache commit_getter lookups
|
||||
commit_cache = {}
|
||||
def _get_annotation(commit_id, commit_getter):
|
||||
if commit_id not in commit_cache:
|
||||
commit_cache[commit_id] = commit_getter()
|
||||
return commit_cache[commit_id]
|
||||
|
||||
annotation_lookup = {
|
||||
line_no: _get_annotation(commit_id, commit_getter)
|
||||
for line_no, commit_id, commit_getter, line_content
|
||||
in filenode.annotate
|
||||
}
|
||||
|
||||
annotations_lines = ((annotation_lookup.get(line_no), line_no, tokens)
|
||||
for line_no, tokens
|
||||
in filenode_as_lines_tokens(filenode))
|
||||
|
||||
grouped_annotations_lines = groupby(annotations_lines, lambda x: x[0])
|
||||
|
||||
for annotation, group in grouped_annotations_lines:
|
||||
yield (
|
||||
annotation, [(line_no, tokens)
|
||||
for (_, line_no, tokens) in group]
|
||||
)
|
||||
|
|
@ -67,7 +67,6 @@ from webhelpers.html.tags import _set_input_attrs, _set_id_attr, \
|
|||
convert_boolean_attrs, NotGiven, _make_safe_id_component
|
||||
from webhelpers2.number import format_byte_size
|
||||
|
||||
from rhodecode.lib.annotate import annotate_highlight
|
||||
from rhodecode.lib.action_parser import action_parser
|
||||
from rhodecode.lib.ext_json import json
|
||||
from rhodecode.lib.utils import repo_name_slug, get_custom_lexer
|
||||
|
|
@ -125,17 +124,18 @@ def asset(path, ver=None):
|
|||
'rhodecode:public/{}'.format(path), _query=query)
|
||||
|
||||
|
||||
def html_escape(text, html_escape_table=None):
|
||||
default_html_escape_table = {
|
||||
ord('&'): u'&',
|
||||
ord('<'): u'<',
|
||||
ord('>'): u'>',
|
||||
ord('"'): u'"',
|
||||
ord("'"): u''',
|
||||
}
|
||||
|
||||
|
||||
def html_escape(text, html_escape_table=default_html_escape_table):
|
||||
"""Produce entities within text."""
|
||||
if not html_escape_table:
|
||||
html_escape_table = {
|
||||
"&": "&",
|
||||
'"': """,
|
||||
"'": "'",
|
||||
">": ">",
|
||||
"<": "<",
|
||||
}
|
||||
return "".join(html_escape_table.get(c, c) for c in text)
|
||||
return text.translate(html_escape_table)
|
||||
|
||||
|
||||
def chop_at_smart(s, sub, inclusive=False, suffix_if_chopped=None):
|
||||
|
|
@ -500,6 +500,86 @@ def get_matching_line_offsets(lines, terms):
|
|||
return matching_lines
|
||||
|
||||
|
||||
def hsv_to_rgb(h, s, v):
|
||||
""" Convert hsv color values to rgb """
|
||||
|
||||
if s == 0.0:
|
||||
return v, v, v
|
||||
i = int(h * 6.0) # XXX assume int() truncates!
|
||||
f = (h * 6.0) - i
|
||||
p = v * (1.0 - s)
|
||||
q = v * (1.0 - s * f)
|
||||
t = v * (1.0 - s * (1.0 - f))
|
||||
i = i % 6
|
||||
if i == 0:
|
||||
return v, t, p
|
||||
if i == 1:
|
||||
return q, v, p
|
||||
if i == 2:
|
||||
return p, v, t
|
||||
if i == 3:
|
||||
return p, q, v
|
||||
if i == 4:
|
||||
return t, p, v
|
||||
if i == 5:
|
||||
return v, p, q
|
||||
|
||||
|
||||
def unique_color_generator(n=10000, saturation=0.10, lightness=0.95):
|
||||
"""
|
||||
Generator for getting n of evenly distributed colors using
|
||||
hsv color and golden ratio. It always return same order of colors
|
||||
|
||||
:param n: number of colors to generate
|
||||
:param saturation: saturation of returned colors
|
||||
:param lightness: lightness of returned colors
|
||||
:returns: RGB tuple
|
||||
"""
|
||||
|
||||
golden_ratio = 0.618033988749895
|
||||
h = 0.22717784590367374
|
||||
|
||||
for _ in xrange(n):
|
||||
h += golden_ratio
|
||||
h %= 1
|
||||
HSV_tuple = [h, saturation, lightness]
|
||||
RGB_tuple = hsv_to_rgb(*HSV_tuple)
|
||||
yield map(lambda x: str(int(x * 256)), RGB_tuple)
|
||||
|
||||
|
||||
def color_hasher(n=10000, saturation=0.10, lightness=0.95):
|
||||
"""
|
||||
Returns a function which when called with an argument returns a unique
|
||||
color for that argument, eg.
|
||||
|
||||
:param n: number of colors to generate
|
||||
:param saturation: saturation of returned colors
|
||||
:param lightness: lightness of returned colors
|
||||
:returns: css RGB string
|
||||
|
||||
>>> color_hash = color_hasher()
|
||||
>>> color_hash('hello')
|
||||
'rgb(34, 12, 59)'
|
||||
>>> color_hash('hello')
|
||||
'rgb(34, 12, 59)'
|
||||
>>> color_hash('other')
|
||||
'rgb(90, 224, 159)'
|
||||
"""
|
||||
|
||||
color_dict = {}
|
||||
cgenerator = unique_color_generator(
|
||||
saturation=saturation, lightness=lightness)
|
||||
|
||||
def get_color_string(thing):
|
||||
if thing in color_dict:
|
||||
col = color_dict[thing]
|
||||
else:
|
||||
col = color_dict[thing] = cgenerator.next()
|
||||
return "rgb(%s)" % (', '.join(col))
|
||||
|
||||
return get_color_string
|
||||
|
||||
|
||||
def get_lexer_safe(mimetype=None, filepath=None):
|
||||
"""
|
||||
Tries to return a relevant pygments lexer using mimetype/filepath name,
|
||||
|
|
@ -536,92 +616,6 @@ def pygmentize(filenode, **kwargs):
|
|||
CodeHtmlFormatter(**kwargs)))
|
||||
|
||||
|
||||
def pygmentize_annotation(repo_name, filenode, **kwargs):
|
||||
"""
|
||||
pygmentize function for annotation
|
||||
|
||||
:param filenode:
|
||||
"""
|
||||
|
||||
color_dict = {}
|
||||
|
||||
def gen_color(n=10000):
|
||||
"""generator for getting n of evenly distributed colors using
|
||||
hsv color and golden ratio. It always return same order of colors
|
||||
|
||||
:returns: RGB tuple
|
||||
"""
|
||||
|
||||
def hsv_to_rgb(h, s, v):
|
||||
if s == 0.0:
|
||||
return v, v, v
|
||||
i = int(h * 6.0) # XXX assume int() truncates!
|
||||
f = (h * 6.0) - i
|
||||
p = v * (1.0 - s)
|
||||
q = v * (1.0 - s * f)
|
||||
t = v * (1.0 - s * (1.0 - f))
|
||||
i = i % 6
|
||||
if i == 0:
|
||||
return v, t, p
|
||||
if i == 1:
|
||||
return q, v, p
|
||||
if i == 2:
|
||||
return p, v, t
|
||||
if i == 3:
|
||||
return p, q, v
|
||||
if i == 4:
|
||||
return t, p, v
|
||||
if i == 5:
|
||||
return v, p, q
|
||||
|
||||
golden_ratio = 0.618033988749895
|
||||
h = 0.22717784590367374
|
||||
|
||||
for _ in xrange(n):
|
||||
h += golden_ratio
|
||||
h %= 1
|
||||
HSV_tuple = [h, 0.95, 0.95]
|
||||
RGB_tuple = hsv_to_rgb(*HSV_tuple)
|
||||
yield map(lambda x: str(int(x * 256)), RGB_tuple)
|
||||
|
||||
cgenerator = gen_color()
|
||||
|
||||
def get_color_string(commit_id):
|
||||
if commit_id in color_dict:
|
||||
col = color_dict[commit_id]
|
||||
else:
|
||||
col = color_dict[commit_id] = cgenerator.next()
|
||||
return "color: rgb(%s)! important;" % (', '.join(col))
|
||||
|
||||
def url_func(repo_name):
|
||||
|
||||
def _url_func(commit):
|
||||
author = commit.author
|
||||
date = commit.date
|
||||
message = tooltip(commit.message)
|
||||
|
||||
tooltip_html = ("<div style='font-size:0.8em'><b>Author:</b>"
|
||||
" %s<br/><b>Date:</b> %s</b><br/><b>Message:"
|
||||
"</b> %s<br/></div>")
|
||||
|
||||
tooltip_html = tooltip_html % (author, date, message)
|
||||
lnk_format = '%5s:%s' % ('r%s' % commit.idx, commit.short_id)
|
||||
uri = link_to(
|
||||
lnk_format,
|
||||
url('changeset_home', repo_name=repo_name,
|
||||
revision=commit.raw_id),
|
||||
style=get_color_string(commit.raw_id),
|
||||
class_='tooltip',
|
||||
title=tooltip_html
|
||||
)
|
||||
|
||||
uri += '\n'
|
||||
return uri
|
||||
return _url_func
|
||||
|
||||
return literal(annotate_highlight(filenode, url_func(repo_name), **kwargs))
|
||||
|
||||
|
||||
def is_following_repo(repo_name, user_id):
|
||||
from rhodecode.model.scm import ScmModel
|
||||
return ScmModel().is_following_repo(repo_name, user_id)
|
||||
|
|
|
|||
|
|
@ -45,7 +45,8 @@
|
|||
@font-family-sans-serif: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
@font-family-serif: Georgia, "Times New Roman", Times, serif;
|
||||
//** Default monospace fonts for `<code>`, `<kbd>`, and `<pre>`.
|
||||
@font-family-monospace: Menlo, Monaco, Consolas, "Courier New", monospace;
|
||||
@font-family-monospace: Consolas, "Liberation Mono", Menlo, Monaco, Courier, monospace;
|
||||
|
||||
@font-family-base: @font-family-sans-serif;
|
||||
|
||||
@font-size-base: 14px;
|
||||
|
|
|
|||
|
|
@ -506,20 +506,19 @@ div.codeblock {
|
|||
|
||||
th,
|
||||
td {
|
||||
padding: .5em !important;
|
||||
border: @border-thickness solid @border-default-color !important;
|
||||
padding: .5em;
|
||||
border: @border-thickness solid @border-default-color;
|
||||
}
|
||||
}
|
||||
|
||||
table {
|
||||
width: 0 !important;
|
||||
border: 0px !important;
|
||||
border: 0px;
|
||||
margin: 0;
|
||||
letter-spacing: normal;
|
||||
|
||||
|
||||
|
||||
|
||||
td {
|
||||
border: 0px !important;
|
||||
border: 0px;
|
||||
vertical-align: top;
|
||||
}
|
||||
}
|
||||
|
|
@ -569,7 +568,7 @@ div.annotatediv { margin-left: 2px; margin-right: 4px; }
|
|||
.CodeMirror ::-moz-selection { background: @rchighlightblue; }
|
||||
|
||||
.code { display: block; border:0px !important; }
|
||||
.code-highlight,
|
||||
.code-highlight, /* TODO: dan: merge codehilite into code-highlight */
|
||||
.codehilite {
|
||||
.hll { background-color: #ffffcc }
|
||||
.c { color: #408080; font-style: italic } /* Comment */
|
||||
|
|
@ -641,3 +640,110 @@ pre.literal-block, .codehilite pre{
|
|||
.border-radius(@border-radius);
|
||||
background-color: @grey7;
|
||||
}
|
||||
|
||||
|
||||
/* START NEW CODE BLOCK CSS */
|
||||
|
||||
table.cb {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 10px;
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* intentionally general selector since .cb-line-selected must override it
|
||||
and they both use !important since the td itself may have a random color
|
||||
generated by annotation blocks. TLDR: if you change it, make sure
|
||||
annotated block selection and line selection in file view still work */
|
||||
.cb-line-fresh .cb-content {
|
||||
background: white !important;
|
||||
}
|
||||
|
||||
tr.cb-annotate {
|
||||
border-top: 1px solid #eee;
|
||||
|
||||
&+ .cb-line {
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
border-top: none;
|
||||
&+ .cb-line {
|
||||
border-top: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
td {
|
||||
vertical-align: top;
|
||||
padding: 2px 10px;
|
||||
|
||||
&.cb-content {
|
||||
white-space: pre-wrap;
|
||||
font-family: @font-family-monospace;
|
||||
font-size: 12.35px;
|
||||
|
||||
span {
|
||||
word-break: break-word;
|
||||
}
|
||||
}
|
||||
|
||||
&.cb-lineno {
|
||||
padding: 0;
|
||||
height: 1px; /* this allows the <a> link to fill to 100% height of the td */
|
||||
width: 50px;
|
||||
color: rgba(0, 0, 0, 0.3);
|
||||
text-align: right;
|
||||
border-right: 1px solid #eee;
|
||||
font-family: @font-family-monospace;
|
||||
|
||||
a::before {
|
||||
content: attr(data-line-no);
|
||||
}
|
||||
&.cb-line-selected {
|
||||
background: @comment-highlight-color !important;
|
||||
}
|
||||
|
||||
a {
|
||||
display: block;
|
||||
height: 100%;
|
||||
color: rgba(0, 0, 0, 0.3);
|
||||
padding: 0 10px; /* vertical padding is 0 so that height: 100% works */
|
||||
line-height: 18px; /* use this instead of vertical padding */
|
||||
}
|
||||
}
|
||||
|
||||
&.cb-content {
|
||||
&.cb-line-selected {
|
||||
background: @comment-highlight-color !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.cb-annotate-info {
|
||||
width: 320px;
|
||||
min-width: 320px;
|
||||
max-width: 320px;
|
||||
padding: 5px 2px;
|
||||
font-size: 13px;
|
||||
|
||||
strong.cb-annotate-message {
|
||||
padding: 5px 0;
|
||||
white-space: pre-line;
|
||||
display: inline-block;
|
||||
}
|
||||
.rc-user {
|
||||
float: none;
|
||||
padding: 0 6px 0 17px;
|
||||
min-width: auto;
|
||||
min-height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
&.cb-annotate-revision {
|
||||
cursor: pointer;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -266,7 +266,35 @@ $(document).ready(function() {
|
|||
}
|
||||
});
|
||||
|
||||
$('.compare_view_files').on(
|
||||
$('body').on( /* TODO: replace the $('.compare_view_files').on('click') below
|
||||
when new diffs are integrated */
|
||||
'click', '.cb-lineno a', function(event) {
|
||||
|
||||
if ($(this).attr('data-line-no') !== ""){
|
||||
$('.cb-line-selected').removeClass('cb-line-selected');
|
||||
var td = $(this).parent();
|
||||
td.addClass('cb-line-selected'); // line number td
|
||||
td.next().addClass('cb-line-selected'); // line content td
|
||||
|
||||
// Replace URL without jumping to it if browser supports.
|
||||
// Default otherwise
|
||||
if (history.pushState) {
|
||||
var new_location = location.href.rstrip('#');
|
||||
if (location.hash) {
|
||||
new_location = new_location.replace(location.hash, "");
|
||||
}
|
||||
|
||||
// Make new anchor url
|
||||
new_location = new_location + $(this).attr('href');
|
||||
history.pushState(true, document.title, new_location);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$('.compare_view_files').on( /* TODO: replace this with .cb function above
|
||||
when new diffs are integrated */
|
||||
'click', 'tr.line .lineno a',function(event) {
|
||||
if ($(this).text() != ""){
|
||||
$('tr.line').removeClass('selected');
|
||||
|
|
@ -365,10 +393,11 @@ $(document).ready(function() {
|
|||
// Select the line that comes from the url anchor
|
||||
// At the time of development, Chrome didn't seem to support jquery's :target
|
||||
// element, so I had to scroll manually
|
||||
if (location.hash) {
|
||||
|
||||
if (location.hash) { /* TODO: dan: remove this and replace with code block
|
||||
below when new diffs are ready */
|
||||
var result = splitDelimitedHash(location.hash);
|
||||
var loc = result.loc;
|
||||
var remainder = result.remainder;
|
||||
if (loc.length > 1){
|
||||
var lineno = $(loc+'.lineno');
|
||||
if (lineno.length > 0){
|
||||
|
|
@ -378,11 +407,70 @@ $(document).ready(function() {
|
|||
tr[0].scrollIntoView();
|
||||
|
||||
$.Topic('/ui/plugins/code/anchor_focus').prepareOrPublish({
|
||||
tr:tr,
|
||||
remainder:remainder});
|
||||
tr: tr,
|
||||
remainder: result.remainder});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (location.hash) { /* TODO: dan: use this to replace the code block above
|
||||
when new diffs are ready */
|
||||
var result = splitDelimitedHash(location.hash);
|
||||
var loc = result.loc;
|
||||
if (loc.length > 1) {
|
||||
var page_highlights = loc.substring(
|
||||
loc.indexOf('#') + 1).split('L');
|
||||
|
||||
if (page_highlights.length > 1) {
|
||||
var highlight_ranges = page_highlights[1].split(",");
|
||||
var h_lines = [];
|
||||
for (var pos in highlight_ranges) {
|
||||
var _range = highlight_ranges[pos].split('-');
|
||||
if (_range.length === 2) {
|
||||
var start = parseInt(_range[0]);
|
||||
var end = parseInt(_range[1]);
|
||||
if (start < end) {
|
||||
for (var i = start; i <= end; i++) {
|
||||
h_lines.push(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
h_lines.push(parseInt(highlight_ranges[pos]));
|
||||
}
|
||||
}
|
||||
for (pos in h_lines) {
|
||||
var line_td = $('td.cb-lineno#L' + h_lines[pos]);
|
||||
if (line_td.length) {
|
||||
line_td.addClass('cb-line-selected'); // line number td
|
||||
line_td.next().addClass('cb-line-selected'); // line content
|
||||
}
|
||||
}
|
||||
var first_line_td = $('td.cb-lineno#L' + h_lines[0]);
|
||||
if (first_line_td.length) {
|
||||
var elOffset = first_line_td.offset().top;
|
||||
var elHeight = first_line_td.height();
|
||||
var windowHeight = $(window).height();
|
||||
var offset;
|
||||
|
||||
if (elHeight < windowHeight) {
|
||||
offset = elOffset - ((windowHeight / 4) - (elHeight / 2));
|
||||
}
|
||||
else {
|
||||
offset = elOffset;
|
||||
}
|
||||
$(function() { // let browser scroll to hash first, then
|
||||
// scroll the line to the middle of page
|
||||
setTimeout(function() {
|
||||
$('html, body').animate({ scrollTop: offset });
|
||||
}, 100);
|
||||
});
|
||||
$.Topic('/ui/plugins/code/anchor_focus').prepareOrPublish({
|
||||
lineno: first_line_td,
|
||||
remainder: result.remainder});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
collapsableContent();
|
||||
});
|
||||
|
|
|
|||
70
rhodecode/templates/codeblocks/source.html
Normal file
70
rhodecode/templates/codeblocks/source.html
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
<%def name="render_line(line_num, tokens,
|
||||
annotation=None,
|
||||
bgcolor=None)">
|
||||
<%
|
||||
# avoid module lookups for performance
|
||||
from rhodecode.lib.codeblocks import pygment_token_class
|
||||
from rhodecode.lib.helpers import html_escape
|
||||
%>
|
||||
<tr class="cb-line cb-line-fresh"
|
||||
%if annotation:
|
||||
data-revision="${annotation.revision}"
|
||||
%endif
|
||||
>
|
||||
<td class="cb-lineno" id="L${line_num}">
|
||||
<a data-line-no="${line_num}" href="#L${line_num}"></a>
|
||||
</td>
|
||||
<td class="cb-content cb-content-fresh"
|
||||
%if bgcolor:
|
||||
style="background: ${bgcolor}"
|
||||
%endif
|
||||
>${
|
||||
''.join(
|
||||
'<span class="%s">%s</span>' %
|
||||
(pygment_token_class(token_type), html_escape(token_text))
|
||||
for token_type, token_text in tokens) + '\n' | n
|
||||
}</td>
|
||||
## this ugly list comp is necessary for performance
|
||||
</tr>
|
||||
</%def>
|
||||
|
||||
<%def name="render_annotation_lines(annotation, lines, color_hasher)">
|
||||
<%
|
||||
rowspan = len(lines) + 1 # span the line's <tr> and annotation <tr>
|
||||
%>
|
||||
%if not annotation:
|
||||
<tr class="cb-annotate">
|
||||
<td class="cb-annotate-message" rowspan="${rowspan}"></td>
|
||||
<td class="cb-annotate-revision" rowspan="${rowspan}"></td>
|
||||
</tr>
|
||||
%else:
|
||||
<tr class="cb-annotate">
|
||||
<td class="cb-annotate-info tooltip"
|
||||
rowspan="${rowspan}"
|
||||
title="Author: ${annotation.author | entity}<br>Date: ${annotation.date}<br>Message: ${annotation.message | entity}"
|
||||
>
|
||||
${h.gravatar_with_user(annotation.author, 16) | n}
|
||||
<strong class="cb-annotate-message">${
|
||||
h.truncate(annotation.message, len(lines) * 30)
|
||||
}</strong>
|
||||
</td>
|
||||
<td
|
||||
class="cb-annotate-revision"
|
||||
rowspan="${rowspan}"
|
||||
data-revision="${annotation.revision}"
|
||||
onclick="$('[data-revision=${annotation.revision}]').toggleClass('cb-line-fresh')"
|
||||
style="background: ${color_hasher(annotation.raw_id)}">
|
||||
<a href="${h.url('changeset_home',repo_name=c.repo_name,revision=annotation.raw_id)}">
|
||||
r${annotation.revision}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
%endif
|
||||
|
||||
%for line_num, tokens in lines:
|
||||
${render_line(line_num, tokens,
|
||||
bgcolor=color_hasher(annotation and annotation.raw_id or ''),
|
||||
annotation=annotation,
|
||||
)}
|
||||
%endfor
|
||||
</%def>
|
||||
|
|
@ -147,43 +147,6 @@
|
|||
if (source_page) {
|
||||
// variants for with source code, not tree view
|
||||
|
||||
if (location.href.indexOf('#') != -1) {
|
||||
page_highlights = location.href.substring(location.href.indexOf('#') + 1).split('L');
|
||||
if (page_highlights.length == 2) {
|
||||
highlight_ranges = page_highlights[1].split(",");
|
||||
|
||||
var h_lines = [];
|
||||
for (pos in highlight_ranges) {
|
||||
var _range = highlight_ranges[pos].split('-');
|
||||
if (_range.length == 2) {
|
||||
var start = parseInt(_range[0]);
|
||||
var end = parseInt(_range[1]);
|
||||
if (start < end) {
|
||||
for (var i = start; i <= end; i++) {
|
||||
h_lines.push(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
h_lines.push(parseInt(highlight_ranges[pos]));
|
||||
}
|
||||
}
|
||||
|
||||
for (pos in h_lines) {
|
||||
// @comment-highlight-color
|
||||
$('#L' + h_lines[pos]).css('background-color', '#ffd887');
|
||||
}
|
||||
|
||||
var _first_line = $('#L' + h_lines[0]).get(0);
|
||||
if (_first_line) {
|
||||
var line = $('#L' + h_lines[0]);
|
||||
if (line.length > 0){
|
||||
offsetScroll(line, 70);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// select code link event
|
||||
$("#hlcode").mouseup(getSelectionLink);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
<%namespace name="sourceblock" file="/codeblocks/source.html"/>
|
||||
|
||||
<div id="codeblock" class="codeblock">
|
||||
<div class="codeblock-header">
|
||||
|
|
@ -51,12 +52,22 @@
|
|||
</div>
|
||||
%else:
|
||||
% if c.file.size < c.cut_off_limit:
|
||||
%if c.annotate:
|
||||
${h.pygmentize_annotation(c.repo_name,c.file,linenos=True,anchorlinenos=True,lineanchors='L',cssclass="code-highlight")}
|
||||
%elif c.renderer:
|
||||
%if c.renderer and not c.annotate:
|
||||
${h.render(c.file.content, renderer=c.renderer)}
|
||||
%else:
|
||||
${h.pygmentize(c.file,linenos=True,anchorlinenos=True,lineanchors='L',cssclass="code-highlight")}
|
||||
<table class="cb codehilite">
|
||||
%if c.annotate:
|
||||
<% color_hasher = h.color_hasher() %>
|
||||
%for annotation, lines in c.annotated_lines:
|
||||
${sourceblock.render_annotation_lines(annotation, lines, color_hasher)}
|
||||
%endfor
|
||||
%else:
|
||||
%for line_num, tokens in c.lines:
|
||||
${sourceblock.render_line(line_num, tokens)}
|
||||
%endfor
|
||||
%endif
|
||||
</table>
|
||||
</div>
|
||||
%endif
|
||||
%else:
|
||||
${_('File is too big to display')} ${h.link_to(_('Show as raw'),
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ def _commit_change(
|
|||
return commit
|
||||
|
||||
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("app")
|
||||
class TestFilesController:
|
||||
|
||||
|
|
@ -270,9 +270,9 @@ class TestFilesController:
|
|||
annotate=True))
|
||||
|
||||
expected_revisions = {
|
||||
'hg': 'r356:25213a5fbb04',
|
||||
'git': 'r345:c994f0de03b2',
|
||||
'svn': 'r208:209',
|
||||
'hg': 'r356',
|
||||
'git': 'r345',
|
||||
'svn': 'r208',
|
||||
}
|
||||
response.mustcontain(expected_revisions[backend.alias])
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue