Index: trac/htdocs/js/keyboard_nav.js
===================================================================
--- trac/htdocs/js/keyboard_nav.js	(revision 7675)
+++ trac/htdocs/js/keyboard_nav.js	(working copy)
@@ -5,18 +5,22 @@
   $(document).keydown(function(event) {
     if (!ENABLE_KEY_NAV)
       return true;
+    if (event.ctrlKey)
+      return true; // let CTRL+R do its job
     var selection = SELECTED_FILE_ELEM;
     switch (event.keyCode) {
-      case 74: // j
+      case 74: // j - next line
         if (selection == null) {
           selection = $('#f0');
+          if ( !selection.length ) 
+            selection = $($("#dirlist tr").get(1))
         } else {
           do {
             selection = selection.next();
           } while (selection.length > 0 && selection.css('display') == 'none');
         }
         break;
-      case 75: // k
+      case 75: // k - previous line
         if (selection == null) {
           selection = $('#f0');
         } else {
@@ -26,17 +30,36 @@
         }
         break;
       case 13: // Enter
-      case 79: // o
+      case 65: // 'a'nnotate
+      case 79: // 'o'pen
+      case 82: // 'r'eload
         if (selection != null) {
           var expander = selection.find('.expander');
           if (expander.length > 0) {
+            if (event.keyCode == 82) {
+              selection.removeClass("expanded").removeClass("collapsed")
+                .siblings("tr."+selection.get(0).id).not(selection).remove();
+            }
             expander.click();
           } else {
-            window.location = selection.find('a.file').attr('href');
+            var href = selection.find('a.file').attr('href');
+            if (href) {
+              if (event.keyCode == 65)
+                href += '?annotate=blame';
+            } else {
+              href = selection.find('a.parent').attr('href');
+            }
+            if (href)
+              window.location = href;
           }
         }
         return false;
         break;
+      case 76: // 'l'og
+        if (selection != null) {
+          window.location = selection.find('td.rev a').attr('href');
+        }
+        break;
       default:
         return true;
     }
Index: trac/htdocs/js/trac.js
===================================================================
--- trac/htdocs/js/trac.js	(revision 7675)
+++ trac/htdocs/js/trac.js	(working copy)
@@ -47,6 +47,10 @@
     });
   }
   
+  $.template = function(str, dict) { 
+    return str.replace(/\${?(\w+)}?/g, function(_, k) { return dict[k]; }); 
+  }
+
   // Used for dynamically updating the height of a textarea
   window.resizeTextArea = function (id, rows) {
     var textarea = $("#" + id).get(0);
@@ -72,4 +76,4 @@
     return $(elem).parents(tagName).get(0);
   }
 
-})(jQuery);
\ No newline at end of file
+})(jQuery);
Index: trac/htdocs/js/expand_dir.js
===================================================================
--- trac/htdocs/js/expand_dir.js	(revision 7675)
+++ trac/htdocs/js/expand_dir.js	(working copy)
@@ -4,18 +4,30 @@
   var FOLDERID_COUNTER = 0;
   var SUBFOLDER_INDENT = 20;
   
-  // enableExpandDir adds the capability to folder rows to be expanded and folded
+  // enableExpandDir adds the capability to ''folder'' rows in a table
+  // to be expanded and folded.
+  //
   // It also teach the rows about their ancestors. It expects:
   //  - `parent_tr`, the logical parent row (`null` if there's no ancestor)
   //  - a `rows` jQuery object matching the newly created entry rows
   //  - `qargs`, additional parameters to send to the server when expanding
+  //  - `autoexpand`, an optional array corresponding to a splitted sub-path
+  //    of entries that will be expanded automatically.
   
-  window.enableExpandDir = function(parent_tr, rows, qargs) {
+  window.enableExpandDir = function(parent_tr, rows, qargs, autoexpand) {
     // the ancestors folder ids are present in the parent_tr class attribute
     var ancestor_folderids = [];
-    if (parent_tr)
+
+    if (parent_tr) // rows are logical children of the parent_tr row
       ancestor_folderids = $.grep(parent_tr.attr("class").split(" "), 
                                   function(c) { return c.match(/^f\d+$/)});
+    else { // rows are toplevel rows, this is the initial call
+      var anchor = window.location.hash.substr(1);
+      if (anchor)
+        autoexpand = anchor.split("/");
+    }
+
+    var autoexpand_expander = null;
     rows.each(function () {
       var a = $(this).find("a.dir");
   
@@ -26,34 +38,51 @@
         $(this).addClass(folderid);
   
         // add the expander icon
-        a.wrap('<div></div>');
-        var expander = a.before('<span class="expander">&nbsp;</span>').prev();
-        expander.attr("title", "Expand sub-directory in place")
-          .click(function() { toggleDir($(this), qargs); });
+        var expander = $('<span class="expander">&nbsp;</span>')
+          .attr("title", "Expand sub-directory in place")
+          .click(function() { toggleDir($(this), qargs); })
+        a.wrap('<div></div>').before(expander);
+        if (autoexpand && a.text() == autoexpand[0])
+          autoexpand_expander = expander;
       }
   
       // tie that row to ancestor folders
       if (parent_tr)
         $(this).addClass(ancestor_folderids.join(" "));
     });
+    
+    if ( autoexpand_expander )
+      toggleDir(autoexpand_expander, qargs, autoexpand.slice(1));
   }
   
   // handler for click event on the expander icons
-  window.toggleDir = function(expander, qargs) {
+  window.toggleDir = function(expander, qargs, autoexpand) {
     var tr = expander.parents("tr:first");
     var folderid = tr.get(0).id;
   
-    if ( tr.filter(".expanded").length ) { // then *fold*
-      tr.removeClass("expanded").addClass("collapsed");
-      tr.siblings("tr."+folderid).hide();
+    if ( tr.hasClass("expanded") ) { // then *fold*
+      tr.removeClass("expanded");
+      if (tr.next().hasClass("error")) {
+        tr.next().remove();
+      } else {
+        tr.addClass("collapsed");
+        tr.siblings("tr."+folderid).hide();
+      }
       expander.attr("title", "Re-expand directory");
       return;
     }
-  
-    if ( tr.filter(".collapsed").length ) { // then *expand*
+
+    // update location, unless autoexpand in progress
+    var a = expander.next("a");
+    if ( !autoexpand )
+      window.location.hash = a.attr("href")
+        .substr(window.location.pathname.length+1)
+        .replace(/([^?]*)(\?.*)?$/, '$1');    
+
+    if ( tr.hasClass("collapsed") ) { // then *expand*
       tr.removeClass("collapsed").addClass("expanded");
       tr.siblings("tr."+folderid).show();
-      // Note that the above will show all the already fetched subtree,
+      // Note that the above will show all the already fetched subtrees,
       // so we have to fold again the folders which were already collapsed.
       tr.siblings("tr.collapsed").each(function() {
         tr.siblings("tr."+this.id).not(this).hide();
@@ -61,19 +90,25 @@
     } else {                                // then *fetch*
       var td = expander.parents("td");
       var td_class = td.attr("class");
-      var a = expander.next("a");
       var depth = 
         parseFloat(td.css("padding-left").replace(/^(\d*\.\d*).*$/, "$1")) + 
         SUBFOLDER_INDENT;
   
       tr.addClass("expanded");
       // insert "Loading ..." row
-      tr.after('<tr><td><span class="loading"></span></td></tr>');
-      var loading_row = tr.next();
-      loading_row.children("td").addClass(td_class)
-        .attr("colspan", tr.children("td").length)
-        .css("padding-left", depth);
-      loading_row.find("span.loading").text("Loading " + a.text() + "...");
+      var loading_row = $($.template(
+        '<tr>'+
+        ' <td class="$td_class" colspan="$colspan" '+
+        '     style="padding-left: ${depth}px">'+
+        '  <span class="loading">Loading $entry...</span>'+
+        ' </td>'+
+        '</tr>', {
+        td_class: td_class, 
+        colspan: tr.children("td").length, 
+        depth: depth, 
+        entry: a.text()
+      }));
+      tr.after(loading_row);
   
       // XHR for getting the rows corresponding to the folder entries
       $.ajax({
@@ -91,22 +126,22 @@
               row = $(this+"</tr>");
               row.children("td."+td_class).css("padding-left", depth);
               // make all entry rows collapsible but only subdir rows expandable
-              enableExpandDir(tr, row, qargs); 
               loading_row.before(row);
+              enableExpandDir(tr, row, qargs, autoexpand); 
             });
             // remove "Loading ..." row
             loading_row.remove();
           } else {
-            loading_row.find("span.loading").text("").append("<i>(empty)</i>")
-              .removeClass("loading");
-            // make the (empty) row collapsible
-            enableExpandDir(tr, loading_row, qargs); 
+            loading_row.find("span.loading")
+              .text("").append("<i>(empty)</i>").removeClass("loading");
+            enableExpandDir(tr, loading_row, qargs); // make it collapsible
           }
         },
         error: function(req, err, exc) {
-          loading_row.find("span.loading").text("").append("<i>(error)</i>")
-            .removeClass("loading");
-          enableExpandDir(tr, loading_row, qargs);
+          loading_row.find("span.loading")
+            .text("").append("<i>(error)</i>").removeClass("loading");
+          loading_row.addClass("error");
+          enableExpandDir(tr, loading_row, qargs); // make it collapsible
         }
       });
     }

