Project Manager for Sublime Text 3 . mirror of https://github.com/budRich/ProjectManager
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

529 lines
19 KiB

  1. import sublime
  2. import sublime_plugin
  3. import subprocess
  4. import os
  5. import codecs
  6. import platform
  7. import re
  8. def plugin_loaded():
  9. t = sublime.load_settings("Project Manager.sublime-settings")
  10. s = sublime.load_settings("project_manager.sublime-settings")
  11. keys = [
  12. "projects_path",
  13. "use_local_projects_dir",
  14. "show_open_files",
  15. "show_recent_projects_first"
  16. ]
  17. d = {}
  18. for k in keys:
  19. if t.has(k) and not s.has(k):
  20. d.update({k: t.get(k)})
  21. for key, value in d.items():
  22. s.set(key, value)
  23. if d:
  24. sublime.save_settings("project_manager.sublime-settings")
  25. old_file = os.path.join(sublime.packages_path(), "User", "Project Manager.sublime-settings")
  26. if os.path.exists(old_file):
  27. os.unlink(old_file)
  28. class JsonFile:
  29. def __init__(self, fpath, encoding="utf-8"):
  30. self.encoding = encoding
  31. self.fpath = fpath
  32. def load(self, default=[]):
  33. self.fdir = os.path.dirname(self.fpath)
  34. if not os.path.isdir(self.fdir):
  35. os.makedirs(self.fdir)
  36. if os.path.exists(self.fpath):
  37. f = codecs.open(self.fpath, "r+", encoding=self.encoding)
  38. content = f.read()
  39. try:
  40. data = sublime.decode_value(content)
  41. except:
  42. sublime.message_dialog("%s is bad!" % self.fpath)
  43. raise
  44. if not data:
  45. data = default
  46. f.close()
  47. else:
  48. f = codecs.open(self.fpath, "w+", encoding=self.encoding)
  49. data = default
  50. f.close()
  51. return data
  52. def save(self, data, indent=4):
  53. self.fdir = os.path.dirname(self.fpath)
  54. if not os.path.isdir(self.fdir):
  55. os.makedirs(self.fdir)
  56. f = codecs.open(self.fpath, "w+", encoding=self.encoding)
  57. f.write(sublime.encode_value(data, True))
  58. f.close()
  59. def remove(self):
  60. if os.path.exists(self.fpath):
  61. os.remove(self.fpath)
  62. def subl(args=[]):
  63. # learnt from SideBarEnhancements
  64. executable_path = sublime.executable_path()
  65. if sublime.platform() == 'osx':
  66. app_path = executable_path[:executable_path.rfind(".app/") + 5]
  67. executable_path = app_path + "Contents/SharedSupport/bin/subl"
  68. subprocess.Popen([executable_path] + args)
  69. if sublime.platform() == "windows":
  70. def fix_focus():
  71. window = sublime.active_window()
  72. view = window.active_view()
  73. window.run_command('focus_neighboring_group')
  74. window.focus_view(view)
  75. sublime.set_timeout(fix_focus, 300)
  76. def expand_folder(folder, project_file):
  77. root = os.path.dirname(project_file)
  78. if not os.path.isabs(folder):
  79. folder = os.path.abspath(os.path.join(root, folder))
  80. return folder
  81. def get_node():
  82. if sublime.platform() == "osx":
  83. node = subprocess.check_output(["scutil", "--get", "ComputerName"]).decode().strip()
  84. else:
  85. node = platform.node().split(".")[0]
  86. return node
  87. def dont_close_windows_when_empty(func):
  88. def f(*args, **kwargs):
  89. preferences = sublime.load_settings("Preferences.sublime-settings")
  90. close_windows_when_empty = preferences.get("close_windows_when_empty")
  91. preferences.set("close_windows_when_empty", False)
  92. func(*args, **kwargs)
  93. if close_windows_when_empty:
  94. preferences.set("close_windows_when_empty", close_windows_when_empty)
  95. return f
  96. class Manager:
  97. def __init__(self, window):
  98. self.window = window
  99. settings_file = 'project_manager.sublime-settings'
  100. self.settings = sublime.load_settings(settings_file)
  101. default_projects_dir = os.path.join(sublime.packages_path(), "User", "Projects")
  102. self.projects_path = self.settings.get(
  103. "projects_path", [self.settings.get("projects_dir", default_projects_dir)])
  104. self.projects_path = [
  105. os.path.normpath(os.path.expanduser(d)) for d in self.projects_path]
  106. node = get_node()
  107. if self.settings.get("use_local_projects_dir", False):
  108. self.projects_path = \
  109. [d + " - " + node for d in self.projects_path] + self.projects_path
  110. self.primary_dir = self.projects_path[0]
  111. self.projects_info = self.get_all_projects_info()
  112. def list_project_files(self, folder):
  113. pfiles = []
  114. library = os.path.join(folder, "library.json")
  115. if os.path.exists(library):
  116. j = JsonFile(library)
  117. for f in j.load([]):
  118. if os.path.exists(f) and f not in pfiles:
  119. pfiles.append(os.path.normpath(f))
  120. pfiles.sort()
  121. j.save(pfiles)
  122. for path, dirs, files in os.walk(folder, followlinks=True):
  123. for f in files:
  124. f = os.path.join(path, f)
  125. if f.endswith(".sublime-project") and f not in pfiles:
  126. pfiles.append(os.path.normpath(f))
  127. # remove empty directories
  128. for d in dirs:
  129. d = os.path.join(path, d)
  130. if len(os.listdir(d)) == 0:
  131. os.rmdir(d)
  132. return pfiles
  133. def get_info_from_project_file(self, pfile):
  134. pdir = self.which_project_dir(pfile)
  135. if pdir:
  136. pname = re.sub("\.sublime-project$", "", os.path.relpath(pfile, pdir))
  137. else:
  138. pname = re.sub("\.sublime-project$", "", os.path.basename(pfile))
  139. pd = JsonFile(pfile).load()
  140. if pd and "folders" in pd and pd["folders"]:
  141. folder = pd["folders"][0].get("path", "")
  142. else:
  143. folder = ""
  144. star = False
  145. for w in sublime.windows():
  146. if w.project_file_name() == pfile:
  147. star = True
  148. break
  149. return {
  150. pname: {
  151. "folder": expand_folder(folder, pfile),
  152. "file": pfile,
  153. "star": star
  154. }
  155. }
  156. def get_all_projects_info(self):
  157. ret = {}
  158. for pdir in self.projects_path:
  159. pfiles = self.list_project_files(pdir)
  160. for f in pfiles:
  161. ret.update(self.get_info_from_project_file(f))
  162. return ret
  163. def which_project_dir(self, pfile):
  164. for pdir in self.projects_path:
  165. if (os.path.realpath(os.path.dirname(pfile))+os.path.sep).startswith(
  166. os.path.realpath(pdir)+os.path.sep):
  167. return pdir
  168. return None
  169. def display_projects(self):
  170. plist = [[key, key + "*" if value["star"] else key, value["folder"], value["file"]]
  171. for key, value in self.projects_info.items()]
  172. plist = sorted(plist)
  173. if self.settings.get("show_recent_projects_first", True):
  174. j = JsonFile(os.path.join(self.primary_dir, "recent.json"))
  175. recent = j.load([])
  176. plist = sorted(plist, key=lambda p: recent.index(p[3]) if p[3] in recent else -1,
  177. reverse=True)
  178. count = 0
  179. for i in range(len(plist)):
  180. if plist[i][0] is not plist[i][1]:
  181. plist.insert(count, plist.pop(i))
  182. count = count + 1
  183. return [item[0] for item in plist], [[item[1], item[2]] for item in plist]
  184. def project_file_name(self, project):
  185. return self.projects_info[project]["file"]
  186. def project_workspace(self, project):
  187. return re.sub("\.sublime-project$", ".sublime-workspace", self.project_file_name(project))
  188. def update_recent(self, project):
  189. j = JsonFile(os.path.join(self.primary_dir, "recent.json"))
  190. recent = j.load([])
  191. pname = self.project_file_name(project)
  192. if pname not in recent:
  193. recent.append(pname)
  194. else:
  195. recent.append(recent.pop(recent.index(pname)))
  196. # only keep the most recent 50 records
  197. if len(recent) > 50:
  198. recent = recent[(50-len(recent)):len(recent)]
  199. j.save(recent)
  200. def clear_recent_projects(self):
  201. def clear_callback():
  202. ok = sublime.ok_cancel_dialog("Clear Recent Projects?")
  203. if ok:
  204. j = JsonFile(os.path.join(self.primary_dir, "recent.json"))
  205. j.remove()
  206. sublime.set_timeout(clear_callback, 100)
  207. def get_project_data(self, project):
  208. return JsonFile(self.project_file_name(project)).load()
  209. def check_project(self, project):
  210. wsfile = self.project_workspace(project)
  211. j = JsonFile(wsfile)
  212. if not os.path.exists(wsfile):
  213. j.save({})
  214. elif self.settings.has("show_open_files"):
  215. show_open_files = self.settings.get("show_open_files", False)
  216. data = j.load({})
  217. data["show_open_files"] = show_open_files
  218. df = data.get("distraction_free", {})
  219. df["show_open_files"] = show_open_files
  220. data["distraction_free"] = df
  221. j.save(data)
  222. @dont_close_windows_when_empty
  223. def close_project_by_window(self, window):
  224. window.run_command("close_workspace")
  225. def close_project_by_name(self, project):
  226. pfile = os.path.realpath(self.project_file_name(project))
  227. for w in sublime.windows():
  228. if w.project_file_name():
  229. if os.path.realpath(w.project_file_name()) == pfile:
  230. self.close_project_by_window(w)
  231. if w.id() != sublime.active_window().id():
  232. w.run_command("close_window")
  233. return True
  234. return False
  235. def add_project(self):
  236. @dont_close_windows_when_empty
  237. def close_all_files():
  238. self.window.run_command("close_all")
  239. def add_callback(project):
  240. pd = self.window.project_data()
  241. f = os.path.join(self.primary_dir, "%s.sublime-project" % project)
  242. if pd:
  243. JsonFile(f).save(pd)
  244. else:
  245. JsonFile(f).save({})
  246. JsonFile(re.sub("\.sublime-project$", ".sublime-workspace", f)).save({})
  247. self.close_project_by_window(self.window)
  248. self.window.run_command("close_project")
  249. close_all_files()
  250. # reload projects info
  251. self.__init__(self.window)
  252. self.switch_project(project)
  253. def show_input_panel():
  254. project = "New Project"
  255. pd = self.window.project_data()
  256. pf = self.window.project_file_name()
  257. try:
  258. path = pd["folders"][0]["path"]
  259. if pf:
  260. project = os.path.basename(expand_folder(path, pf))
  261. else:
  262. project = os.path.basename(path)
  263. except:
  264. pass
  265. v = self.window.show_input_panel("Project name:", project, add_callback, None, None)
  266. v.run_command("select_all")
  267. sublime.set_timeout(show_input_panel, 100)
  268. def import_sublime_project(self):
  269. pfile = self.window.project_file_name()
  270. if not pfile:
  271. sublime.message_dialog("Project file not found!")
  272. return
  273. if self.which_project_dir(pfile):
  274. sublime.message_dialog("This project was created by Project Manager!")
  275. return
  276. ok = sublime.ok_cancel_dialog("Import %s?" % os.path.basename(pfile))
  277. if ok:
  278. j = JsonFile(os.path.join(self.primary_dir, "library.json"))
  279. data = j.load([])
  280. if pfile not in data:
  281. data.append(pfile)
  282. j.save(data)
  283. def append_project(self, project):
  284. self.update_recent(project)
  285. pd = self.get_project_data(project)
  286. paths = [expand_folder(f.get("path"), self.project_file_name(project))
  287. for f in pd.get("folders")]
  288. subl(["-a"] + paths)
  289. def switch_project(self, project):
  290. self.update_recent(project)
  291. self.check_project(project)
  292. self.close_project_by_window(self.window)
  293. self.close_project_by_name(project)
  294. subl([self.project_file_name(project)])
  295. def open_in_new_window(self, project):
  296. self.update_recent(project)
  297. self.check_project(project)
  298. self.close_project_by_name(project)
  299. subl(["-n", self.project_file_name(project)])
  300. def _remove_project(self, project):
  301. ok = sublime.ok_cancel_dialog("Remove \"%s\" from Project Manager?" % project)
  302. if ok:
  303. pfile = self.project_file_name(project)
  304. if self.which_project_dir(pfile):
  305. self.close_project_by_name(project)
  306. os.unlink(self.project_file_name(project))
  307. os.unlink(self.project_workspace(project))
  308. else:
  309. for pdir in self.projects_path:
  310. j = JsonFile(os.path.join(pdir, "library.json"))
  311. data = j.load([])
  312. if pfile in data:
  313. data.remove(pfile)
  314. j.save(data)
  315. sublime.status_message("Project \"%s\" is removed." % project)
  316. def remove_project(self, project):
  317. sublime.set_timeout(lambda: self._remove_project(project), 100)
  318. def clean_dead_projects(self):
  319. projects_to_remove = []
  320. for pname, pi in self.projects_info.items():
  321. folder = pi["folder"]
  322. if not os.path.exists(folder):
  323. projects_to_remove.append(pname)
  324. def remove_projects_iteratively():
  325. pname = projects_to_remove[0]
  326. self._remove_project(pname)
  327. projects_to_remove.remove(pname)
  328. if len(projects_to_remove) > 0:
  329. sublime.set_timeout(remove_projects_iteratively, 100)
  330. if len(projects_to_remove) > 0:
  331. sublime.set_timeout(remove_projects_iteratively, 100)
  332. else:
  333. sublime.message_dialog("No Dead Projects.")
  334. def edit_project(self, project):
  335. def on_open():
  336. self.window.open_file(self.project_file_name(project))
  337. sublime.set_timeout_async(on_open, 100)
  338. def rename_project(self, project):
  339. def rename_callback(new_project):
  340. if project == new_project:
  341. return
  342. pfile = self.project_file_name(project)
  343. wsfile = self.project_workspace(project)
  344. pdir = self.which_project_dir(pfile)
  345. if not pdir:
  346. pdir = os.path.dirname(pfile)
  347. new_pfile = os.path.join(pdir, "%s.sublime-project" % new_project)
  348. new_wsfile = re.sub("\.sublime-project$", ".sublime-workspace", new_pfile)
  349. reopen = self.close_project_by_name(project)
  350. os.rename(pfile, new_pfile)
  351. os.rename(wsfile, new_wsfile)
  352. j = JsonFile(new_wsfile)
  353. data = j.load({})
  354. if "project" in data:
  355. data["project"] = "%s.sublime-project" % os.path.basename(new_project)
  356. j.save(data)
  357. if not self.which_project_dir(pfile):
  358. for pdir in self.projects_path:
  359. library = os.path.join(pdir, "library.json")
  360. if os.path.exists(library):
  361. j = JsonFile(library)
  362. data = j.load([])
  363. if pfile in data:
  364. data.remove(pfile)
  365. data.append(new_pfile)
  366. j.save(data)
  367. if reopen:
  368. # reload projects info
  369. self.__init__(self.window)
  370. self.open_in_new_window(new_project)
  371. def show_input_panel():
  372. v = self.window.show_input_panel("New project name:",
  373. project, rename_callback, None, None)
  374. v.run_command("select_all")
  375. sublime.set_timeout(show_input_panel, 100)
  376. def cancellable(func):
  377. def _ret(self, action):
  378. if action >= 0:
  379. func(self, action)
  380. elif action < 0 and self.caller == "manager":
  381. sublime.set_timeout(self.run, 10)
  382. return _ret
  383. class ProjectManager(sublime_plugin.WindowCommand):
  384. def show_quick_panel(self, items, on_done):
  385. sublime.set_timeout(
  386. lambda: self.window.show_quick_panel(items, on_done),
  387. 10)
  388. def run(self, action=None, caller=None):
  389. self.manager = Manager(self.window)
  390. if action is None:
  391. self.show_options()
  392. elif action == "add_project":
  393. self.manager.add_project()
  394. elif action == "import_sublime_project":
  395. self.manager.import_sublime_project()
  396. elif action == "clear_recent_projects":
  397. self.manager.clear_recent_projects()
  398. elif action == "remove_dead_projects":
  399. self.manager.clean_dead_projects()
  400. else:
  401. self.caller = caller
  402. callback = eval("self.on_" + action)
  403. self.projects, display = self.manager.display_projects()
  404. if not self.projects:
  405. sublime.message_dialog("Project list is empty.")
  406. return
  407. self.show_quick_panel(display, callback)
  408. def show_options(self):
  409. items = [
  410. ["Open Project", "Open project in the current window"],
  411. ["Open Project in New Window", "Open project in a new window"],
  412. ["Append Project", "Append project to current window"],
  413. ["Edit Project", "Edit project settings"],
  414. ['Rename Project', "Rename project"],
  415. ["Remove Project", "Remove from Project Manager"],
  416. ["Add New Project", "Add current folders to Project Manager"],
  417. ["Import Project", "Import current .sublime-project file"],
  418. ["Clear Recent Projects", "Clear Recent Projects"],
  419. ["Remove Dead Projects", "Remove Dead Projects"]
  420. ]
  421. def callback(a):
  422. if a < 0:
  423. return
  424. elif a <= 5:
  425. actions = ["switch", "new", "append", "edit", "rename", "remove"]
  426. self.run(action=actions[a], caller="manager")
  427. elif a == 6:
  428. self.run(action="add_project")
  429. elif a == 7:
  430. self.run(action="import_sublime_project")
  431. elif a == 8:
  432. self.run(action="clear_recent_projects")
  433. elif a == 9:
  434. self.run(action="remove_dead_projects")
  435. self.show_quick_panel(items, callback)
  436. @cancellable
  437. def on_new(self, action):
  438. self.manager.open_in_new_window(self.projects[action])
  439. @cancellable
  440. def on_switch(self, action):
  441. self.manager.switch_project(self.projects[action])
  442. @cancellable
  443. def on_append(self, action):
  444. self.manager.append_project(self.projects[action])
  445. @cancellable
  446. def on_remove(self, action):
  447. self.manager.remove_project(self.projects[action])
  448. @cancellable
  449. def on_edit(self, action):
  450. self.manager.edit_project(self.projects[action])
  451. @cancellable
  452. def on_rename(self, action):
  453. self.manager.rename_project(self.projects[action])