Github Open files in Editor

Adds a button next to files on Github to quickly open it in your favorite IDE

  1. // ==UserScript==
  2. // @name Github Open files in Editor
  3. // @namespace http://tampermonkey.net/GithubOpenFilesInEditor
  4. // @version 0.2
  5. // @description Adds a button next to files on Github to quickly open it in your favorite IDE
  6. // @author Alexandre Blanc
  7. // @match https://github.com/*
  8. // @icon https://www.google.com/s2/favicons?sz=64&domain=undefined.localhost
  9. // @grant none
  10. // @license MIT
  11. // ==/UserScript==
  12.  
  13. // For PhpStorm users, it requires plugin : IDE Remote Control
  14. // For VSCode users on Mac OS, please add in your terminal:
  15. // defaults write com.google.Chrome URLAllowlist -array "vscode://*"
  16.  
  17. (function() {
  18. 'use strict';
  19. let userSettings = {projectList: {}, editor: ''};
  20. let currentUrl = '';
  21. let project = '';
  22.  
  23. const loadSettings = function() {
  24. if (!window.localStorage.getItem('ghofieSettings')) {
  25. return {projectList: {}, editor: 'phpstorm'};
  26. }
  27. userSettings = JSON.parse(window.localStorage.getItem('ghofieSettings'));
  28. }
  29.  
  30. const saveSettings = function() {
  31. window.localStorage.setItem('ghofieSettings', JSON.stringify(userSettings))
  32. removeLinks();
  33. setLinks();
  34. }
  35.  
  36. const handleUrlChange = function() {
  37. if (window.location.href === currentUrl) {
  38. return;
  39. }
  40. window.setTimeout(initSettingsPanel, 500);
  41. removeLinks();
  42. setLinks();
  43. currentUrl = window.location.href;
  44. }
  45.  
  46. const initUrlChangeDetection = function() {
  47. setInterval(function() {
  48. handleUrlChange();
  49. },1000);
  50. }
  51.  
  52. const icon = function (color) {
  53. return `<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" x="0px" y="0px" viewBox="0 0 1000 1000" xml:space="preserve">
  54. <g>
  55. <g transform="translate(0.000000,511.000000) scale(0.100000,-0.100000)" fill="red">
  56. <path d="M267.3,4393.3c-47.8-23.9-100.4-76.5-124.3-124.3c-40.6-78.9-43-327.5-43-4159s2.4-4080.2,43-4159c23.9-47.8,76.5-100.4,124.3-124.3c78.9-40.6,353.8-43,4732.7-43c4378.9,0,4653.8,2.4,4732.7,43c47.8,23.9,100.4,76.5,124.3,124.3c40.6,78.9,43,327.5,43,4159s-2.4,4080.2-43,4159c-23.9,47.8-76.5,100.4-124.3,124.3c-78.9,40.6-353.8,43-4732.7,43C621.1,4436.3,346.2,4433.9,267.3,4393.3z M9314.4,2990.2v-860.5H5000H685.6l-7.2,836.6c-2.4,458.9,0,850.9,7.2,865.3c7.2,26.3,874.8,31.1,4319.2,26.3l4309.6-7.2V2990.2z M9321.6-1042.1l-7.2-2588.6H5000H685.6l-7.2,2588.6l-4.8,2586.2H5000h4326.3L9321.6-1042.1z"/>
  57. <path d="M8284.2,3140.8l-45.4-55l55,45.4c28.7,26.3,52.6,50.2,52.6,55C8346.3,3205.4,8327.2,3191,8284.2,3140.8z"/>
  58. <path d="M5425.5,927.5c-38.2-21.5-81.3-71.7-100.4-112.3c-16.7-38.2-294-855.7-611.9-1811.8c-621.5-1869.2-614.3-1845.3-499.6-1967.2c112.3-119.5,329.8-117.1,425.5,4.8c55,71.7,1221.4,3554.3,1221.4,3649.9C5860.5,905.9,5609.5,1044.6,5425.5,927.5z"/>
  59. <path d="M2248.8-227c-894-600-944.1-635.8-975.2-729c-31.1-88.4-28.7-105.2,11.9-191.2c40.6-81.3,162.5-172.1,965.7-707.5c800.7-537.8,932.2-616.7,1011.1-616.7c124.3,0,217.5,52.6,265.3,153c47.8,100.4,47.8,136.2,4.8,241.4c-28.7,66.9-148.2,157.8-741,552.1c-389.6,260.5-707.5,478-707.5,485.2c0,7.2,315.5,224.7,700.3,480.4c595.2,399.2,705.1,480.4,743.4,556.9c38.2,78.9,40.6,107.6,16.7,181.7c-35.9,124.3-121.9,196-248.6,210.3C3193,401.6,3181,394.4,2248.8-227z"/>
  60. <path d="M6601.5,365.8c-76.5-38.2-167.3-174.5-167.3-253.4c0-138.6,78.9-207.9,786.4-676.4c382.4-253.4,695.5-468.5,695.5-475.6c0-7.2-317.9-224.7-707.5-485.2c-592.8-394.4-712.3-485.2-741-552.1c-43-105.2-43-141,4.8-241.4c47.8-100.4,141-153,265.3-153c78.9,0,210.3,78.9,1011.1,616.7c1032.6,688.4,1039.8,695.6,975.2,896.3c-31.1,98-57.4,117.1-970.4,729C6766.4,427.9,6747.3,439.8,6601.5,365.8z"/>
  61. </g>
  62. </g>
  63. </svg>`
  64. };
  65.  
  66. document.phpStormLink = function (link) {
  67. const hrefLink = `http://localhost:63342/api/file?file=${link}`;
  68. const linkWindow = window.open(hrefLink,'autoOpenInEditor');
  69. setTimeout(function() {
  70. linkWindow.close();
  71. },1000);
  72. };
  73.  
  74. document.vsCodeLink = function (link) {
  75. const hrefLink = `vscode://file/${link}`;
  76. const linkWindow = window.open(hrefLink,'autoOpenInEditor');
  77. setTimeout(function() {
  78. linkWindow.close();
  79. },1000);
  80. };
  81.  
  82. const setLinks = function() {
  83. const tabnavs = document.querySelectorAll('.tabnav [data-pjax="#repo-content-pjax-container"] .tabnav-tab');
  84. if (tabnavs.length === 0) {
  85. return;
  86. }
  87.  
  88. project = document.querySelector('.AppHeader-context-full ul li:last-child a').href.split('/').pop();
  89.  
  90. switch (Array.from(document.querySelectorAll('.tabnav-tabs[aria-label="Pull request tabs"] .tabnav-tab')).findIndex(el => el.classList.contains("selected"))) {
  91. case 0: console.log('setLinksInConversationTab'); setLinksInConversationTab(); break;
  92. case 1: console.log('setLinksInCommitsTab'); setLinksInCommitsTab(); break;
  93. case 3: console.log('setLinksInFilesChangedTab'); setLinksInFilesChangedTab(); break;
  94. }
  95. }
  96.  
  97. const handleClickOnOpenInEditorLink = function (link) {
  98. const projectPath = userSettings.projectList[project] || '';
  99. switch (userSettings.editor) {
  100. case 'phpstorm': document.phpStormLink(projectPath + link); break;
  101. case 'vscode': document.vsCodeLink(projectPath + link); break;
  102. }
  103. }
  104.  
  105. const setLinksInConversationTab = function() {
  106. let linkCounter = 0;
  107. document.querySelectorAll('turbo-frame[id^="review-thread-or-comment-id-"] summary a').forEach(function (a) {
  108. linkCounter++;
  109. const fileUrl = a.innerHTML;
  110. const ghofieLinkId = `js-open-in-phpstorm-${linkCounter}`;
  111. a.insertAdjacentHTML('afterend', `
  112. <div
  113. id="${ghofieLinkId}"
  114. title="Open file in editor"
  115. data-link="${fileUrl}"
  116. class="ghofie-link"
  117. style="margin: 2px 5px 0px 0px"
  118. >
  119. ${icon()}
  120. </div>
  121. `);
  122.  
  123. document.getElementById(ghofieLinkId).addEventListener('click',function(e){
  124. e.preventDefault();
  125. handleClickOnOpenInEditorLink(e.currentTarget.dataset.link);
  126.  
  127. });
  128. });
  129. }
  130.  
  131. const setLinksInCommitsTab = function() {
  132. let linkCounter = 0;
  133. document.querySelectorAll(
  134. 'diff-layout #files div.js-file div.file-header span.Truncate clipboard-copy'
  135. ).forEach(function (e) {
  136. linkCounter++;
  137. const fileUrl = e.value;
  138. const ghofieLinkId = `js-open-in-phpstorm-${linkCounter}`;
  139. e.insertAdjacentHTML('afterend', `
  140. <div
  141. id="${ghofieLinkId}"
  142. title="Open file in editor"
  143. data-link="${fileUrl}"
  144. class="ghofie-link"
  145. style="margin: 2px 5px 0px 0px; cursor: pointer;"
  146. >
  147. ${icon()}
  148. </div>
  149. `);
  150.  
  151. document.getElementById(ghofieLinkId).addEventListener('click',function(e){
  152. e.preventDefault();
  153. handleClickOnOpenInEditorLink(e.currentTarget.dataset.link);
  154.  
  155. });
  156. });
  157. }
  158.  
  159. const setLinksInFilesChangedTab = function() {
  160. let linkCounter = 0;
  161. document.querySelectorAll(
  162. 'div[data-target="diff-layout.mainContainer"] #files div.js-file div.file-header span.Truncate clipboard-copy'
  163. ).forEach(function (e) {
  164. linkCounter++;
  165. const fileUrl = e.value;
  166. const ghofieLinkId = `js-open-in-phpstorm-${linkCounter}`;
  167. e.insertAdjacentHTML('afterend', `
  168. <div
  169. id="${ghofieLinkId}"
  170. title="Open file in editor"
  171. data-link="${fileUrl}"
  172. class="ghofie-link"
  173. style="margin: 2px 5px 0px 0px; cursor: pointer;"
  174. >
  175. ${icon()}
  176. </div>
  177. `);
  178.  
  179. document.getElementById(ghofieLinkId).addEventListener('click',function(e){
  180. e.preventDefault();
  181. handleClickOnOpenInEditorLink(e.currentTarget.dataset.link);
  182.  
  183. });
  184. });
  185. }
  186.  
  187. const removeLinks = function() {
  188. document.querySelectorAll('.ghofie-link').forEach(function (v) {
  189. v.parentElement.removeChild(v);
  190. });
  191. }
  192.  
  193. const isConfigurationValid = function() {
  194. try {
  195. JSON.parse(document.getElementById("ghofie-project-list").value);
  196. return true;
  197. } catch (e) {
  198. return false;
  199. }
  200. };
  201.  
  202. const toggleSettingsDisplay = function() {
  203. const element = document.getElementById('ghofie-settings');
  204. element.style.display = element.style.display == 'none' ? 'inline-block' : 'none'
  205. if (element.style.display == 'inline-block') {
  206. document.getElementById("ghofie-project-list").value = JSON.stringify(userSettings.projectList);
  207. document.getElementById('ghofie-editor-choice-' + userSettings.editor).checked = true;
  208. }
  209. };
  210.  
  211. const initSettingsPanel = function() {
  212. if (document.querySelector('#ghofie-settings') !== null) {
  213. document.querySelector('#ghofie-settings').remove();
  214. document.querySelector('#js-btn-ghofie-settings').remove();
  215. }
  216. document.querySelector('.AppHeader-globalBar').insertAdjacentHTML('afterend', `
  217. <div class="row px-3 px-md-4 px-lg-5" id="ghofie-settings" style="display:none;width:100%;">
  218. <div class="form-group">
  219. <h3>GitHub - Open file in editor</h3>
  220. <label class="label-bold" for="ghofie-project-list">
  221. Project association <i>{"${project}":"/home/src/localpath/"}</i>
  222. </label>
  223. <textarea class="form-control" rows="3" maxlength="650" id="ghofie-project-list" name="ghofie-settings"></textarea>
  224. <label class="label-bold">Editor choice</label>
  225. <div class="form-check">
  226. <input type="radio" class="form-check-input" id="ghofie-editor-choice-phpstorm" name="ghofie-editor-choice" value="phpstorm">
  227. <label for="ghofie-editor-choice-phpstorm" class="form-check-label">PhpStorm</label>
  228. </div>
  229. <div class="form-check">
  230. <input type="radio" class="form-check-input" id="ghofie-editor-choice-vscode" name="ghofie-editor-choice" value="vscode">
  231. <label for="ghofie-editor-choice-vscode" class="form-check-label">VS Code</label>
  232. </div>
  233. <button
  234. id="ghofie-settings-save-button"
  235. class="btn btn-secondary"
  236. >
  237. Save Settings
  238. </button>
  239. </div>
  240. </div>
  241. `);
  242.  
  243. let ghofieSettingsButton = document.createElement('a');
  244. ghofieSettingsButton.addEventListener('click', function (e) {
  245. e.preventDefault();
  246. toggleSettingsDisplay();
  247. return false;
  248. });
  249. ghofieSettingsButton.id = 'js-btn-ghofie-settings';
  250. ghofieSettingsButton.href = '#';
  251. ghofieSettingsButton.innerHTML = `${icon('#919191')}`;
  252. ghofieSettingsButton.classList.add('AppHeader-button');
  253. ghofieSettingsButton.classList.add('Button--secondary');
  254.  
  255. document.querySelector('.AppHeader-globalBar-end notification-indicator').insertAdjacentElement('afterend', ghofieSettingsButton);
  256.  
  257. document.getElementById('ghofie-settings-save-button').addEventListener(
  258. 'click',
  259. function (e) {
  260. e.preventDefault();
  261. if (isConfigurationValid()) {
  262. toggleSettingsDisplay();
  263. userSettings.projectList = JSON.parse(document.getElementById("ghofie-project-list").value);
  264. userSettings.editor = document.querySelector('input[name="ghofie-editor-choice"]:checked').value;
  265. saveSettings();
  266. }
  267. return false;
  268. }
  269. );
  270. };
  271.  
  272.  
  273. loadSettings();
  274. initUrlChangeDetection();
  275. })();