Open GitHub files in VS Code

When viewing a file on a known GitHub repo with a local clone, pressing the `\` key will open the file in VS Code. If a line is highlighted, the file will be opened to that line in VS Code.

当前为 2023-08-18 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Open GitHub files in VS Code
  3. // @version 1.0.1
  4. // @author aminomancer
  5. // @homepageURL https://github.com/aminomancer/userscripts
  6. // @supportURL https://github.com/aminomancer/userscripts
  7. // @namespace https://github.com/aminomancer
  8. // @match https://github.com/*/*
  9. // @grant GM_listValues
  10. // @grant GM_getValue
  11. // @grant GM_setValue
  12. // @description When viewing a file on a known GitHub repo with a local clone, pressing the `\` key will open the file in VS Code. If a line is highlighted, the file will be opened to that line in VS Code.
  13. // @license CC-BY-NC-SA-4.0
  14. // @icon data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1024 998.7'><path d='M512 0C229.1 0 0 229.1 0 512c0 226.6 146.6 417.9 350.1 485.8 25.6 4.5 35.2-10.9 35.2-24.3 0-12.2-.6-52.5-.6-95.4-128.6 23.7-161.9-31.4-172.2-60.2-5.8-14.7-30.7-60.2-52.5-72.3-17.9-9.6-43.5-33.3-.6-33.9 40.3-.6 69.1 37.1 78.7 52.5 46.1 77.4 119.7 55.7 149.1 42.2 4.5-33.3 17.9-55.7 32.6-68.5-113.9-12.8-233-57-233-252.8 0-55.7 19.8-101.8 52.5-137.6-5.1-12.8-23-65.3 5.1-135.7 0 0 42.9-13.4 140.8 52.5 41-11.5 84.5-17.3 128-17.3s87 5.8 128 17.3c97.9-66.6 140.8-52.5 140.8-52.5 28.2 70.4 10.2 122.9 5.1 135.7 32.6 35.8 52.5 81.3 52.5 137.6 0 196.5-119.7 240-233.6 252.8 18.6 16 34.6 46.7 34.6 94.7 0 68.5-.6 123.5-.6 140.8 0 13.4 9.6 29.4 35.2 24.3C877.4 929.9 1024 737.9 1024 512 1024 229.1 794.9 0 512 0z' fill-rule='evenodd' clip-rule='evenodd' fill='white'/></svg>
  15. // ==/UserScript==
  16.  
  17. /* global GM_listValues, GM_getValue, GM_setValue */
  18.  
  19. // These are the default preference values. When the script is first installed,
  20. // these values will be used to populate the preferences, which are stored by
  21. // the userscript manager. To modify the preferences, don't edit the file here.
  22. // Go to the Values tab in the userscript manager and edit them there.
  23. const defaultPrefs = {
  24. // This script works by opening a URL with vscode's custom URL protocol. The
  25. // protocol name can be changed here. The default is "vscode", but if you use
  26. // VS Code Insiders, you should change it to "vscode-insiders".
  27. protocol_name: "vscode",
  28. // This is how the script knows what local file to open. This pref maps each
  29. // GitHub repo to the path of the local clone. If the repo name is "foo/bar",
  30. // then the path should be "/path/to/foo/bar". If a repo is not listed here,
  31. // it will not be opened in VS Code. Only use forward slashes, even on
  32. // Windows, since the path becomes part of a URL.
  33. repos: {
  34. "user123/example456": "/path/to/user123/example456",
  35. },
  36. };
  37.  
  38. for (const [key, value] of Object.entries(defaultPrefs)) {
  39. if (GM_getValue(key) === undefined) {
  40. GM_setValue(key, value);
  41. }
  42. }
  43.  
  44. const prefs = {};
  45. for (const key of GM_listValues()) {
  46. prefs[key] = GM_getValue(key);
  47. }
  48.  
  49. function openInVSCode({ repoName, filePath, lineNum }) {
  50. const repoPath = prefs.repos[repoName];
  51. if (!repoPath) return;
  52. let protocolURL = `${prefs.protocol_name}://file/${repoPath}/${filePath}`;
  53. if (lineNum) {
  54. protocolURL += `:${lineNum}`;
  55. }
  56. if (!protocolURL) return;
  57. var link = document.createElement("a");
  58. link.setAttribute("href", protocolURL);
  59. link.click();
  60. }
  61.  
  62. function getForFilesView() {
  63. let fileView;
  64. let fileHeader;
  65. const hash = location.hash?.match(/#diff-(.*)/)?.[1]?.split("-")[0];
  66. let targetDiff = hash && `diff-${hash}`;
  67. let targetFile = targetDiff && document.getElementById(targetDiff);
  68. while (targetFile) {
  69. if (!targetFile.classList.contains("file")) {
  70. if (targetFile.classList.contains("selected-line")) {
  71. targetFile = targetFile.closest(".file");
  72. continue;
  73. }
  74. break;
  75. }
  76. const header = targetFile.querySelector(".file-header");
  77. const rect = header.getBoundingClientRect();
  78. if (rect.top >= 0 && rect.bottom <= window.innerHeight) {
  79. fileView = targetFile;
  80. fileHeader = header;
  81. }
  82. break;
  83. }
  84.  
  85. if (!fileView) {
  86. const fileHeaders = document.querySelectorAll(".file-header");
  87. for (const header of fileHeaders) {
  88. const rect = header.getBoundingClientRect();
  89. if (
  90. Math.floor(
  91. Math.abs(rect.top - parseInt(getComputedStyle(header).top))
  92. ) === 0
  93. ) {
  94. fileHeader = header;
  95. fileView = fileHeader.closest(".file");
  96. break;
  97. }
  98. }
  99. }
  100.  
  101. if (!fileView) {
  102. return null;
  103. }
  104.  
  105. const selectedLine = fileView.querySelector(".selected-line");
  106. const lineNum = selectedLine?.dataset?.lineNumber;
  107.  
  108. const fileMenu = fileHeader.querySelector(".dropdown details-menu");
  109. let fileDetails;
  110. for (const item of fileMenu.children) {
  111. let path = item.pathname;
  112. if (!path) continue;
  113. const match = path.match(/\/([^/]+)\/([^/]+)\/blob\/([^/]+)\/(.*)/);
  114. if (!match) continue;
  115. const [, user, repo, , filePath] = match;
  116. fileDetails = {};
  117. fileDetails.repoName = `${user}/${repo}`;
  118. fileDetails.filePath = filePath;
  119. fileDetails.lineNum = lineNum;
  120. break;
  121. }
  122.  
  123. return fileDetails;
  124. }
  125.  
  126. function getForURL(url) {
  127. switch (typeof url) {
  128. case "string":
  129. url = new URL(url);
  130. break;
  131. case "object":
  132. if (url instanceof URL) break;
  133. if (url instanceof Location) break;
  134. if (url instanceof HTMLAnchorElement) {
  135. url = new URL(url.href);
  136. break;
  137. }
  138. // fall through
  139. default:
  140. return null;
  141. }
  142. const [, user, repo, , , ...pathParts] = url.pathname.split("/");
  143. if (!pathParts.length) return null;
  144. const repoName = `${user}/${repo}`;
  145. const lineNum = url.hash?.match(/^#L(\d+)/)?.[1];
  146. return { repoName, filePath: pathParts.join("/"), lineNum };
  147. }
  148.  
  149. function handleKeydown(event) {
  150. if (event.key === "\\") {
  151. if (document.querySelector("#files.diff-view")) {
  152. const fileDetails = getForFilesView();
  153. if (!fileDetails) return;
  154. event.preventDefault();
  155. openInVSCode(fileDetails);
  156. } else if (location.pathname.match(/^\/[^/]+\/[^/]+\/blob\//)) {
  157. const fileDetails = getForURL(location);
  158. if (!fileDetails) return;
  159. event.preventDefault();
  160. openInVSCode(fileDetails);
  161. } else if (document.querySelector(".js-navigation-container")) {
  162. const focusedItem = document.querySelector(
  163. ".js-navigation-item.navigation-focus"
  164. );
  165. if (!focusedItem) return;
  166. const rect = focusedItem.getBoundingClientRect();
  167. if (rect.top >= 0 && rect.bottom <= window.innerHeight) {
  168. const link = focusedItem.querySelector("a.rgh-quick-file-edit");
  169. if (!link) return;
  170. const fileDetails = getForURL(link);
  171. if (!fileDetails) return;
  172. event.preventDefault();
  173. openInVSCode(fileDetails);
  174. }
  175. }
  176. }
  177. }
  178.  
  179. document.addEventListener("keydown", handleKeydown);