Greasy Fork is available in English.

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-19 يوللانغان نەشرى. ئەڭ يېڭى نەشرىنى كۆرۈش.

  1. // ==UserScript==
  2. // @name Open GitHub files in VS Code
  3. // @version 1.0.2
  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 https://cdn.jsdelivr.net/gh/aminomancer/userscripts@latest/icons/vscode.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);