[PATCH V7] hgweb: expose a followlines UI in filerevision view

Gregory Szorc gregory.szorc at gmail.com
Fri Mar 31 21:53:28 EDT 2017


On Fri, Mar 31, 2017 at 4:47 AM, Denis Laxalde <denis at laxalde.org> wrote:

> # HG changeset patch
> # User Denis Laxalde <denis.laxalde at logilab.fr>
> # Date 1490819176 -7200
> #      Wed Mar 29 22:26:16 2017 +0200
> # Node ID be7965e3afe82be35d258a2cff12b389a857ef88
> # Parent  dea2a17cbfd00bf08ee87b3e44b1c71499189f89
> # Available At http://hg.logilab.org/users/dlaxalde/hg
> #              hg pull http://hg.logilab.org/users/dlaxalde/hg -r
> be7965e3afe8
> hgweb: expose a followlines UI in filerevision view
>

This looks good to me and I think it can be queued. There is obviously some
follow-up work:

* Add support for gitweb (and other styles if we care)
* Add floating labels (or similar) to help draw attention to the feature
* Add support on the blame page (and anywhere else it makes sense)
* CSS tweaking (if others want to)

But this can be deferred until after landing. Perfect is the enemy of good.

This is an awesome feature and I can wait to use it!

Before I forget, I think someone should record a demo video or an animated
GIF of this to put in the 4.2 release notes. AFAIK no other VCS has this
feature and I think we could turn some heads by calling attention to it.


>
> In filerevision view (/file/<rev>/<fname>) we add some event listeners on
> mouse clicks of <span> elements in the <pre class="sourcelines"> block.
> Those listeners will capture a range of lines selected between two mouse
> clicks and a box inviting to follow the history of selected lines will then
> show up. Selected lines (i.e. the block of lines) get a CSS class which
> make
> them highlighted. Selection can be cancelled (and restarted) by either
> clicking on the cancel ("x") button in the invite box or clicking on any
> other
> source line. Also clicking twice on the same line will abort the selection
> and
> reset event listeners to restart the process.
>
> As a first step, this action is only advertised by the "cursor: cell" CSS
> rule
> on source lines elements as any other mechanisms would make the code
> significantly more complicated. This might be improved later.
>
> All JavaScript code lives in a new "linerangelog.js" file, sourced in
> filerevision template (only in "paper" style for now).
>
> diff --git a/contrib/wix/templates.wxs b/contrib/wix/templates.wxs
> --- a/contrib/wix/templates.wxs
> +++ b/contrib/wix/templates.wxs
> @@ -225,6 +225,7 @@
>              <File Id="static.coal.file.png"      Name="coal-file.png" />
>              <File Id="static.coal.folder.png"    Name="coal-folder.png" />
>              <File Id="static.excanvas.js"        Name="excanvas.js" />
> +            <File Id="static.linerangelog.js"    Name="linerangelog.js" />
>              <File Id="static.mercurial.js"       Name="mercurial.js" />
>              <File Id="static.hgicon.png"         Name="hgicon.png" />
>              <File Id="static.hglogo.png"         Name="hglogo.png" />
> diff --git a/mercurial/templates/paper/filerevision.tmpl
> b/mercurial/templates/paper/filerevision.tmpl
> --- a/mercurial/templates/paper/filerevision.tmpl
> +++ b/mercurial/templates/paper/filerevision.tmpl
> @@ -71,8 +71,11 @@
>  <div class="overflow">
>  <div class="sourcefirst linewraptoggle">line wrap: <a
> class="linewraplink" href="javascript:toggleLinewrap()">on</a></div>
>  <div class="sourcefirst"> line source</div>
> -<pre class="sourcelines stripes4 wrap bottomline">{text%fileline}</pre>
> +<pre class="sourcelines stripes4 wrap bottomline"
> data-logurl="{url|urlescape}log/{symrev}/{file|urlescape}"
> >{text%fileline}</pre>
>  </div>
> +
> +<script type="text/javascript" src="{staticurl|urlescape}
> linerangelog.js"></script>
> +
>  </div>
>  </div>
>
> diff --git a/mercurial/templates/static/linerangelog.js
> b/mercurial/templates/static/linerangelog.js
> new file mode 100644
> --- /dev/null
> +++ b/mercurial/templates/static/linerangelog.js
> @@ -0,0 +1,163 @@
> +// linerangelog.js - JavaScript utilities for followlines UI
> +//
> +// Copyright 2017 Logilab SA <contact at logilab.fr>
> +//
> +// This software may be used and distributed according to the terms of the
> +// GNU General Public License version 2 or any later version.
> +
> +//** Install event listeners for line block selection and followlines
> action */
> +function installLineSelect() {
> +    var sourcelines = document.getElementsByClassName('sourcelines')[0];
> +    if (typeof sourcelines === 'undefined') {
> +        return;
> +    }
> +    // URL to complement with "linerange" query parameter
> +    var targetUri = sourcelines.dataset.logurl;
> +    if (typeof targetUri === 'undefined') {
> +        return;
> +    }
> +
> +    // retrieve all direct <span> children of <pre class="sourcelines">
> +    var spans = Array.prototype.filter.call(
> +        sourcelines.children,
> +        function(x) { return x.tagName === 'SPAN' });
> +
> +    var lineSelectedCSSClass = 'followlines-selected';
> +
> +    //** add CSS class on <span> element in `from`-`to` line range */
> +    function addSelectedCSSClass(from, to) {
> +        for (var i = from; i <= to; i++) {
> +            spans[i].classList.add(lineSelectedCSSClass);
> +        }
> +    }
> +
> +    //** remove CSS class from previously selected lines */
> +    function removeSelectedCSSClass() {
> +        var elements = sourcelines.getElementsByClassName(
> +            lineSelectedCSSClass);
> +        while (elements.length) {
> +            elements[0].classList.remove(lineSelectedCSSClass);
> +        }
> +    }
> +
> +    // ** return the <span> element parent of `element` */
> +    function findParentSpan(element) {
> +        var parent = element.parentElement;
> +        if (parent === null) {
> +            return null;
> +        }
> +        if (element.tagName == 'SPAN' && parent.isSameNode(sourcelines))
> {
> +            return element;
> +        }
> +        return findParentSpan(parent);
> +    }
> +
> +    //** event handler for "click" on the first line of a block */
> +    function lineSelectStart(e) {
> +        var startElement = findParentSpan(e.target);
> +        if (startElement === null) {
> +            // not a <span> (maybe <a>): abort, keeping event listener
> +            // registered for other click with <span> target
> +            return;
> +        }
> +        var startId = parseInt(startElement.id.slice(1));
> +        startElement.classList.add(lineSelectedCSSClass); // CSS
> +
> +        // remove this event listener
> +        sourcelines.removeEventListener('click', lineSelectStart);
> +
> +        //** event handler for "click" on the last line of the block */
> +        function lineSelectEnd(e) {
> +            var endElement = findParentSpan(e.target);
> +            if (endElement === null) {
> +                // not a <span> (maybe <a>): abort, keeping event listener
> +                // registered for other click with <span> target
> +                return;
> +            }
> +
> +            // remove this event listener
> +            sourcelines.removeEventListener('click', lineSelectEnd);
> +
> +            // compute line range (startId, endId)
> +            var endId = parseInt(endElement.id.slice(1));
> +            if (endId == startId) {
> +                // clicked twice the same line, cancel and reset initial
> state
> +                // (CSS and event listener for selection start)
> +                removeSelectedCSSClass();
> +                sourcelines.addEventListener('click', lineSelectStart);
> +                return;
> +            }
> +            var inviteElement = endElement;
> +            if (endId < startId) {
> +                var tmp = endId;
> +                endId = startId;
> +                startId = tmp;
> +                inviteElement = startElement;
> +            }
> +
> +            addSelectedCSSClass(startId - 1, endId -1);  // CSS
> +
> +            // append the <div id="followlines"> element to last line of
> the
> +            // selection block
> +            var divAndButton = followlinesBox(targetUri, startId, endId);
> +            var div = divAndButton[0],
> +                button = divAndButton[1];
> +            inviteElement.appendChild(div);
> +
> +            //** event handler for cancelling selection */
> +            function cancel() {
> +                // remove invite box
> +                div.parentNode.removeChild(div);
> +                // restore initial event listeners
> +                sourcelines.addEventListener('click', lineSelectStart);
> +                sourcelines.removeEventListener('click', cancel);
> +                // remove styles on selected lines
> +                removeSelectedCSSClass();
> +            }
> +
> +            // bind cancel event to click on <button>
> +            button.addEventListener('click', cancel);
> +            // as well as on an click on any source line
> +            sourcelines.addEventListener('click', cancel);
> +        }
> +
> +        sourcelines.addEventListener('click', lineSelectEnd);
> +
> +    }
> +
> +    sourcelines.addEventListener('click', lineSelectStart);
> +
> +}
> +
> +//** return a <div id="followlines"> and inner cancel <button> elements */
> +function followlinesBox(targetUri, fromline, toline) {
> +    // <div id="followlines">
> +    var div = document.createElement('div');
> +    div.id = 'followlines';
> +
> +    //   <div class="followlines-cancel">
> +    var buttonDiv = document.createElement('div');
> +    buttonDiv.classList.add('followlines-cancel');
> +
> +    //     <button>x</button>
> +    var button = document.createElement('button');
> +    button.textContent = 'x';
> +    buttonDiv.appendChild(button);
> +    div.appendChild(buttonDiv);
> +
> +    //   <div class="followlines-link">
> +    var aDiv = document.createElement('div');
> +    aDiv.classList.add('followlines-link');
> +
> +    //     <a href="/log/<rev>/<file>?patch=&linerange=...">
> +    var a = document.createElement('a');
> +    var url = targetUri + '?patch=&linerange=' + fromline + ':' + toline;
> +    a.setAttribute('href', url);
> +    a.textContent = 'follow lines ' + fromline + ':' + toline;
> +    aDiv.appendChild(a);
> +    div.appendChild(aDiv);
> +
> +    return [div, button];
> +}
> +
> +document.addEventListener('DOMContentLoaded', installLineSelect, false);
> diff --git a/mercurial/templates/static/style-paper.css
> b/mercurial/templates/static/style-paper.css
> --- a/mercurial/templates/static/style-paper.css
> +++ b/mercurial/templates/static/style-paper.css
> @@ -280,6 +280,46 @@ td.annotate:hover div.annotate-info { di
>    background-color: #bfdfff;
>  }
>
> +div.overflow pre.sourcelines > span:hover {
> +  cursor: cell;
> +}
> +
> +pre.sourcelines > span.followlines-selected {
> +  background-color: #99C7E9;
> +}
> +
> +div#followlines {
> +  background-color: #B7B7B7;
> +  border: 1px solid #CCC;
> +  border-radius: 5px;
> +  padding: 4px;
> +  position: absolute;
> +}
> +
> +div.followlines-cancel {
> +  text-align: right;
> +}
> +
> +div.followlines-cancel > button {
> +  line-height: 80%;
> +  padding: 0;
> +  border: 0;
> +  border-radius: 2px;
> +  background-color: inherit;
> +  font-weight: bold;
> +}
> +
> +div.followlines-cancel > button:hover {
> +  color: #FFFFFF;
> +  background-color: #CF1F1F;
> +}
> +
> +div.followlines-link {
> +  margin: 2px;
> +  margin-top: 4px;
> +  font-family: sans-serif;
> +}
> +
>  .sourcelines > a {
>      display: inline-block;
>      position: absolute;
> diff --git a/tests/test-hgweb-commands.t b/tests/test-hgweb-commands.t
> --- a/tests/test-hgweb-commands.t
> +++ b/tests/test-hgweb-commands.t
> @@ -1343,9 +1343,12 @@ File-related
>    <div class="overflow">
>    <div class="sourcefirst linewraptoggle">line wrap: <a
> class="linewraplink" href="javascript:toggleLinewrap()">on</a></div>
>    <div class="sourcefirst"> line source</div>
> -  <pre class="sourcelines stripes4 wrap bottomline">
> +  <pre class="sourcelines stripes4 wrap bottomline"
> data-logurl="/log/1/foo">
>    <span id="l1">foo</span><a href="#l1"></a></pre>
>    </div>
> +
> +  <script type="text/javascript" src="/static/linerangelog.js"></script>
> +
>    </div>
>    </div>
>
> @@ -1468,9 +1471,12 @@ File-related
>    <div class="overflow">
>    <div class="sourcefirst linewraptoggle">line wrap: <a
> class="linewraplink" href="javascript:toggleLinewrap()">on</a></div>
>    <div class="sourcefirst"> line source</div>
> -  <pre class="sourcelines stripes4 wrap bottomline">
> +  <pre class="sourcelines stripes4 wrap bottomline"
> data-logurl="/log/2/foo">
>    <span id="l1">another</span><a href="#l1"></a></pre>
>    </div>
> +
> +  <script type="text/javascript" src="/static/linerangelog.js"></script>
> +
>    </div>
>    </div>
>
> _______________________________________________
> Mercurial-devel mailing list
> Mercurial-devel at mercurial-scm.org
> https://www.mercurial-scm.org/mailman/listinfo/mercurial-devel
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://www.mercurial-scm.org/pipermail/mercurial-devel/attachments/20170331/ba81101a/attachment.html>


More information about the Mercurial-devel mailing list