Locate View Partial Template in 1 Click

Problem

When debugging or revamping web pages, we inspect views and partials inside. When complicated page comes in, locating codes can be time consuming, especially when we are not familiar with the page.

The Idea

Chrome extension for finding out partials rendered.

Click the partial name to hit the exact file.

To implement, we need

  1. an automated way to add extra markups
  2. a chrome extension to visualize partials and listen to user action
  3. a background script to launch the text editor

The Markups

The partial info must come from somewhere. Below I take sinatra and erb as an example.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# inside the configure do block (not production)
# override the prepare method of tilt
Tilt::ERBTemplate.class_eval do
  def prepare
    @outvar = options[:outvar] || self.class.default_output_variable
    options[:trim] = '<>' if !(options[:trim] == false) && (options[:trim].nil? || options[:trim] == true)

    unless data.lines.first.start_with? "<!doctype"
      data and data.prepend <<-TAG
        <i class="erb-locator" style="display:none; visibility:hidden">#{file}</i>
      TAG
    end

    @engine = ::ERB.new(data, options[:safe], options[:trim], @outvar)
  end
end unless $production

Now <i> tags should be generated right above any partials rendered.

1
2
3
<i class="erb-locator" style="display:none; visibility:hidden">
  /home/loki/loper/trunk/app/views/prod/main/mock-index.erb
</i>

Hacking ActionView::Renderer#render_partial gives you similar result in rails.

The Chrome Extension

Next we need a chrome extension to visualize those <i> tags. Launch text editor when user clicked on it.

1
2
3
4
5
6
7
8
9
10
11
12
13
// manifest.json

{
  // We have to hardcode a private key here for generating fixed extension ID
  // later our ruby script can be called within this extension
  "key": "...private key...",
  ...
  "permissions": [
    "activeTab",         // permission for getting content from active tab
    "nativeMessaging"    // permission for talking to local scripts
  ],
  ...
}

As you can see, a fixed extension ID and Native Messaging support are required.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// background.js

// Execute the erb locator script when user clicked on a locator
chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
  if (request.erb) {
    console.log("Received", request);
    chrome.runtime.sendNativeMessage('com.loki.erb_locator',
      { erb: request.erb },
      function() {
        console.log("Native Message Sent");
      }
    );
    sendResponse({});
  }
});

// Extension entry point
chrome.browserAction.onClicked.addListener(function(tab) {
  chrome.tabs.executeScript(null, {file:"jquery-1.10.2.min.js"}, function() {
    chrome.tabs.executeScript(null, {file:"inject.js"});
  });
});

We have done 2 things here.

  1. added a listener to trigger the erb locator ruby script
  2. injected inject.js to the active tab when user activating our extension

Finally, inject.js for visualizing locators and listening to user action.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
// inject.js

// the backdrop, we will place our locators on it
var $mask = null;

// Open partial in text editor thru Native Messaging
function openERB(erb) {
  chrome.runtime.sendMessage({erb: erb}, function() {
    console.log("Message Sent: " + erb);
  });
}

// locate and visualize those locator
function buildLinks() {
  // keep track on locators with same position for avoid overlapping
  var count = {};

  $('i.erb-locator').each(function() {
    var $this = $(this);
    var path = $this.text();
    var erbName = path.replace(/.*\/(.*).erb/, "$1");

    var $a = $('<a class="_erb">' + erbName + '</a>').attr({
      "href":     "javascript:void(0)",
      "data-erb": path
    }).css({
      "position":   "absolute",
      "border":     "1px solid #888",
      "padding":    "2px 6px",
      "font-size":  "14px",
      "color":      "#111",
      "background": "white"
    });
    // slightly add offsets to overlapped nodes
    var o = $this.next().offset();
    var k = o.top * 1000 + o.left;
    count[k] = count[k] == undefined ? 0 : count[k] + 1;
    $a.offset({top: o.top + count[k] * 19, left: o.left});
    $mask.append($a);
  });
  $('body').append($mask);
}

// select the backdrop out
$mask = $('#_mask');

// remove it when it is already exist
// user triggered the extension again without selecting any one
if (!!$mask.length) {
  $mask.remove();

} else {
  $mask = $('<div id="_mask">');
  $mask.css({
    "position":         "absolute",
    "top":              "0",
    "left":             "0",
    "right":            "0",
    "bottom":           "0",
    "background-color": "rgba(255, 255, 255, 0.5)",
    "z-index":          "99999"
  });

  buildLinks();
  $mask.on('click', 'a._erb', function() {
    openERB($(this).data('erb'));
    $mask.remove();
    return false;
  });
}

The Backend

Mentioned quite a lot the locator ruby script, below is an example and you may roll out your own.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#!/usr/bin/env ruby

require 'syslog'
require 'json'

Syslog.open

# start here
Syslog.info "Native Message Received, RUBY_VERSION: #{RUBY_VERSION}"

# read number of bytes from stdin
def readc(n)
  n.times.map { STDIN.getc }.join
end

# read msg from chrome native messaging
def readmsg
  len = readc(4).unpack("i")[0]
  readc len
end

# read filename
msg = readmsg
Syslog.info "msg: #{msg}"
filename = JSON.load(msg)["erb"]

system "vim --servername main --remote-send ':tab drop #{filename}<cr><cr>'"
system "wmctrl -a 'GVIM main'"

To let chrome talk to the script, we have to register a Native Messaging Host. For Linux host, place file below under ~/.config/google-chrome/NativeMessagingHosts.

1
2
3
4
5
6
7
8
9
10
// com.loki.erb_locator.json
{
  "name": "com.loki.erb_locator",
  "description": "ERB Locator",
  "path": "/home/loki/loki/env/bin/erb-locator",
  "type": "stdio",
  "allowed_origins": [
    "chrome-extension://odiihfohmigfneenjpfckobhboicfpgn/"
  ]
}

Comments