GitHub - Add Path Search

Enable easy searching in a specific path

  1. // ==UserScript==
  2. // @name GitHub - Add Path Search
  3. // @namespace http://splintor.wordpress.com/
  4. // @version 0.2
  5. // @description Enable easy searching in a specific path
  6. // @author splintor@gmail.com
  7. // @updateUrl https://gist.github.com/splintor/8d3f12b86962efe5dcacb28ca15aa87d/raw
  8. // @match https://github.com/*
  9. // ==/UserScript==
  10.  
  11. 'use strict';
  12.  
  13. const GITHUB_BASE = 'https://github.com';
  14. const SEARCH_ENDPOINT = 'search';
  15.  
  16. function urlGenerator(urlParts, searchCriteria) {
  17. const { org, path, repo } = urlParts;
  18. if (!org) {
  19. return {
  20. base: `${GITHUB_BASE}/${SEARCH_ENDPOINT}`,
  21. query: { q: searchCriteria },
  22. };
  23. }
  24.  
  25. if (!repo) {
  26. return {
  27. base: `${GITHUB_BASE}/${SEARCH_ENDPOINT}`,
  28. query: {
  29. q: `${searchCriteria} org:${org}`,
  30. type: `Code`
  31. }
  32. };
  33. }
  34.  
  35. const query = searchCriteria + (path ? ` path:${path}` : '');
  36. return {
  37. base: `${GITHUB_BASE}/${org}/${repo}/${SEARCH_ENDPOINT}`,
  38. query: { q: query }
  39. };
  40. }
  41.  
  42. function parseUrl(url) {
  43. const regex = /https:\/\/github\.com\/([\w-]*)\/?([\w-\.]*)?(?:\/(tree|blob)\/\w*\/)?(.*)?/;
  44. const match = regex.exec(url) || [];
  45. const path = match[3] === 'blob' ? match[4].split('/').slice(0, -1).join('/') : match[4];
  46. return {
  47. org: match[1] || null,
  48. repo: match[2] || null,
  49. path: path && !path.startsWith('/search?') ? path : null,
  50. };
  51. }
  52.  
  53. function combineQueryParams(params) {
  54. return '?' + Object.keys(params)
  55. .map(key => `${key}=${encodeURIComponent(params[key])}`)
  56. .join('&');
  57. }
  58.  
  59. (function buildElement() {
  60. const container = document.getElementById('jump-to-results');
  61. if (!container) {
  62. setTimeout(buildElement, 100);
  63. return;
  64. }
  65.  
  66. new MutationObserver(function(mutations) {
  67. mutations.forEach(function(mutation) {
  68. if (mutation.type == "childList") {
  69. const searchScoped = Array.from(mutation.addedNodes).find(n => n.id === 'jump-to-suggestion-search-scoped');
  70. if (searchScoped) {
  71. const existing = document.getElementById('jump-to-suggestion-search-path');
  72. if (existing) {
  73. existing.remove();
  74. }
  75.  
  76. const urlParts = parseUrl(location.href);
  77. if (!urlParts.path) {
  78. return;
  79. }
  80.  
  81. const element = searchScoped.cloneNode(true);
  82. element.classList.remove('js-jump-to-scoped-search', 'navigation-focus');
  83. element.classList.add('js-jump-to-path-search');
  84. element.setAttribute('aria-selected', 'false');
  85. element.id = 'jump-to-suggestion-search-path';
  86. const searchString = document.getElementsByName('q')[0].value;
  87. const { base, query } = urlGenerator(urlParts, searchString);
  88. const a = element.querySelector('a');
  89. const url = base + combineQueryParams(query);
  90. a.setAttribute('href', url);
  91. a.addEventListener('click', event => { // we need this because the search input form somehow overrides clicks and uses its action
  92. event.preventDefault();
  93. window.location.href = url;
  94. });
  95. const badge = element.querySelector('.js-jump-to-badge-search-text-default');
  96. const text = 'in ' + urlParts.path;
  97. badge.innerText = text;
  98. badge.setAttribute('aria-label', 'text');
  99. searchScoped.after(element);
  100. }
  101. }
  102. });
  103. }).observe(container, { childList: true });
  104. })();