Browse Source

update package

tags/0.7.3
Johannes Rappen 3 years ago
parent
commit
d2b5b0c9d5
21 changed files with 892 additions and 596 deletions
  1. +12
    -0
      .editorconfig
  2. +41
    -0
      .github/ISSUE_TEMPLATE.md
  3. +6
    -0
      .gitignore
  4. +22
    -0
      .sublimelinterrc
  5. +117
    -0
      CHANGELOG.md
  6. +37
    -33
      README.md
  7. +0
    -17
      closewindow.py
  8. +27
    -1
      commands/Default.sublime-commands
  9. +0
    -0
      inux).sublime-keymap
  10. +0
    -0
      SX).sublime-keymap
  11. +0
    -0
      indows).sublime-keymap
  12. +31
    -17
      menus/Main.sublime-menu
  13. +0
    -528
      pm.py
  14. +5
    -0
      project_manager.py
  15. +0
    -0
      settings/project_manager.sublime-settings
  16. +1
    -0
      src/__init__.py
  17. +25
    -0
      src/fix_old_settings_file.py
  18. +38
    -0
      src/json_file.py
  19. +390
    -0
      src/project_manager.py
  20. +28
    -0
      src/text_commands.py
  21. +112
    -0
      src/window_commands.py

+ 12
- 0
.editorconfig View File

@@ -0,0 +1,12 @@
root = true

[*]
charset = utf-8
end_of_line = lf
indent_style = space
indent_size = 4
insert_final_newline = true
trim_trailing_whitespace = true

[*.md]
indent_size = 2

+ 41
- 0
.github/ISSUE_TEMPLATE.md View File

@@ -0,0 +1,41 @@
# Short and descriptive example bug report title

## Summary

...

> Any other information you want to share that is relevant to the issue being reported. This might include the lines of code that you have identified as causing the bug, and potential solutions (and your opinions on their merits).

## Expected behaviour

...

## Actual behaviour

...

## Steps to reproduce

* This is the first step.
1. one
2. two
3. three
* This is the second step.
* Further steps, etc.

### Environment

```text
ProjectManager:

version: 0.7.2
installed via Package Control: True

Sublime Text:

channel: stable
version: 3126
platform: windows
portable: yes
architecture: x64
```

+ 6
- 0
.gitignore View File

@@ -0,0 +1,6 @@
*.cache
*.log
*.pyc
*.pyo
*.sublime-workspace
.DS_Store

+ 22
- 0
.sublimelinterrc View File

@@ -0,0 +1,22 @@
{
"@python": 3,
"linters":
{
"flake8":
{
"ignore": "F401, F403",
"max-line-length": 120
},
"pep257":
{
"add-ignore":
[
"D202"
]
},
"pep8":
{
"max-line-length": 120
}
}
}

+ 117
- 0
CHANGELOG.md View File

@@ -0,0 +1,117 @@
## CHANGELOG

### Unreleased

* add `.github/ISSUE_TEMPLATE.md`
* add `.editorconfig`
* add `.gitignore`
* add `.sublimelinterrc`
* add `CHANGELOG.md`
* commands
* open settings side-by-side
* open key bindings side-by-side
* menus
* target default entries by id only
* support custom main menus (in other languages)
* open setttings side-by-side
* open key bindings side-by-side
* python source
* split into files and load as module
* put strings in single quotes
* no need to `import codecs`
* use `with` when accessing files
* set newline to `\n` when writing
* if dialog `answer is True`
* open README and CHANGELOG in a read_only, scratch copy in a new tab
* via main menu or command palette

### [0.7.2](https://github.com/randy3k/ProjectManager/compare/0.7.1...0.7.2)

* update README
* add key bindings for windows and linux
* use realpath for detecting window to close
* better name the command as "Add New Project"

### [0.7.1](https://github.com/randy3k/ProjectManager/compare/0.7.0...0.7.1)

* fix #55

### [0.7.0](https://github.com/randy3k/ProjectManager/compare/0.6.11...0.7.0)

* fix typo
* remove show_open_files settings as the bug was fixed
* use re.sub instead of replace to fix #54
* use relative link
* rename as ProjectManager

### [0.6.11](https://github.com/randy3k/ProjectManager/compare/0.6.10...0.6.11)

* redundant caption
* feature: remove dead projects
* change default order of projects list

### [0.6.10](https://github.com/randy3k/ProjectManager/compare/0.6.9...0.6.10)

* cannonicalize projects directories
* update menus and screenshots
* use close_all instead

### [0.6.9](https://github.com/randy3k/ProjectManager/compare/0.6.8...0.6.9)

* close project by window or name

### [0.6.8](https://github.com/randy3k/ProjectManager/compare/0.6.7...0.6.8)

* use try-catch errors
* only when library exists
* no long check close_windows_when_empty
* rename to get_info_from_project_file
* cannonicalize paths to fix #47

### [0.6.7](https://github.com/randy3k/ProjectManager/compare/0.6.6...0.6.7)

* use set_timeout instead of set_timeout_async
* only check library file if it exists
* rename functions for better readability
* only close non-active window

### [0.6.6](https://github.com/randy3k/ProjectManager/compare/0.6.5...0.6.6)

* focus on the original view

### [0.6.5](https://github.com/randy3k/ProjectManager/compare/0.6.4...0.6.5)

* auto refresh folder list
* various updates
* update README
* confirm to clear recent projects
* remove refresh folder functionality
* add emptylink in before / after code block
* fix #2

### [0.6.4](https://github.com/randy3k/ProjectManager/compare/0.6.3...0.6.4)

* show message when project list is empty

### [0.6.3](https://github.com/randy3k/ProjectManager/compare/0.6.2...0.6.3)

* fix window closing behaviour
* fix which_project_dir bug again

### [0.6.2](https://github.com/randy3k/ProjectManager/compare/0.6.1...0.6.2)

* bootstrap manager run function
* resolve symlink
* rename function to expand_folder
* fix which_project_dir

### [0.6.1](https://github.com/randy3k/ProjectManager/compare/0.6.0...0.6.1)

* add `get_project_files()`
* add `get_project_info()`
* don't use timeoout_async
* pep8 fix

### 0.6.0

* first release

+ 37
- 33
README.md View File

@@ -1,70 +1,74 @@
# Project Manager for Sublime Text 3
# [`ProjectManager`](https://github.com/randy3k/ProjectManager) for [Sublime Text](https://www.sublimetext.com)

<a href="https://packagecontrol.io/packages/ProjectManager"><img src="https://packagecontrol.herokuapp.com/downloads/ProjectManager.svg"></a>
<a href="https://www.paypal.com/cgi-bin/webscr?cmd=_donations&amp;business=Randy%2ecs%2elai%40gmail%2ecom&amp;lc=US&amp;item_name=Package&amp;currency_code=USD&amp;bn=PP%2dDonationsBF%3apaypal%2ddonate%2dyellow%2esvg%3aNonHosted" title="Donate to this project using Paypal"><img src="https://img.shields.io/badge/paypal-donate-blue.svg" /></a>
<a href="https://gratipay.com/~randy3k/" title="Donate to this project using Gratipay"><img src="https://img.shields.io/badge/gratipay-donate-yellow.svg" /></a>
[![License](https://img.shields.io/github/license/randy3k/ProjectManager.svg?style=flat-square)](https://github.com/randy3k/ProjectManager/blob/master/LICENSE.txt)
[![Downloads Package Control](https://img.shields.io/packagecontrol/dt/ProjectManager.svg?style=flat-square)](https://packagecontrol.io/packages/ProjectManager)
[![Latest release](https://img.shields.io/github/release/randy3k/ProjectManager.svg?style=flat-square)](https://github.com/randy3k/ProjectManager/releases/latest)
[![Donate via PayPal](https://img.shields.io/badge/paypal-donate-009cde.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=Randy%2ecs%2elai%40gmail%2ecom&lc=US&item_name=Package&currency_code=USD&bn=PP%2dDonationsBF%3apaypal%2ddonate%2dyellow%2esvg%3aNonHosted)
[![Donate via Gratipay](https://img.shields.io/badge/gratipay-donate-yellow.svg)](https://gratipay.com/~randy3k/)

Dont't have any idea what `*.sublime-project` and `*.sublime-workspace` are doing? Forget where the project files are? Don't worry, Project Manager will help organizing the project files by putting them in a centralized location. (It is inspired by Atom's [Project Manager](https://atom.io/packages/project-manager), but Atom's Project Manager is inspired by the built-in Sublime Text Project Manager,
so there is a circular reasoning here).
Dont't have any idea what `*.sublime-project` and `*.sublime-workspace` are doing? Forget where the project files are? Don't worry, Project Manager will help organizing the project files by putting them in a centralized location. (It is inspired by Atom's [Project Manager](https://atom.io/packages/project-manager), but Atom's Project Manager is inspired by the built-in Sublime Text Project Manager, so there is a circular reasoning here).

![](https://cloud.githubusercontent.com/assets/1690993/20858319/7f12a6ec-b911-11e6-8fc5-f4cbf6b6f12b.png)
![Screenshot](https://cloud.githubusercontent.com/assets/1690993/20858319/7f12a6ec-b911-11e6-8fc5-f4cbf6b6f12b.png)

## Requirements

### Installation
ProjectManager targets and is tested against the **latest Build** of Sublime Text.

You can install Project Manager via Package Control.
* [ST3 (stable)](https://www.sublimetext.com/3)
* [ST3 (dev)](https://www.sublimetext.com/3dev)

You can additionally add the following keybind in your user keybind settings file for "Open project in new window"
## Installation

```
{
"keys": ["super+ctrl+o"], // or ["ctrl+alt+o"] for Windows/Linux
"command": "project_manager", "args": {"action": "new"}
}
```
Using **Package Control** is not required, but recommended as it keeps your packages (with their dependencies) up-to-date!

### Installation via Package Control

### Usage
* [Install Package Control](https://packagecontrol.io/installation#st3)
* Close and reopen Sublime Text after having installed Package Control.
* Open the Command Palette (`Tools > Command Palette`).
* Choose `Package Control: Install Package`.
* Search for [`ProjectManager` on Package Control](https://packagecontrol.io/packages/ProjectManager) and select to install.

To launch the Project Manager, you can either open it under the `Project` menu or via the command palette: `Project Manager: ...`.
## Usage

To quick switch between projects, use the hotkey <kbd>Ctrl</kbd>+<kbd>Cmd</kbd>+<kbd>P</kbd> (<kbd>Ctrl</kbd>+<kbd>Alt</kbd>+<kbd>P</kbd> for windows/linux).
To launch ProjectManager, use the main menu (`Project > Project Manager`) or the command palette (`Project Manager: ...`).

Project Manager also improves the shortcut <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>W</kbd> on Windows and Linux so that it will close the project when the window is closed. On OSX, it is the default behaviour.
To quickly switch between projects, use the hotkey <kbd>Ctrl</kbd><kbd>⌘ Cmd</kbd><kbd>P</kbd> on macOS (<kbd>Ctrl</kbd><kbd>Alt</kbd><kbd>P</kbd> on Windows / Linux).

ProjectManager also improves the shortcut <kbd>Ctrl</kbd><kbd>Shift</kbd><kbd>W</kbd> on Windows / Linux so that it will close the project when the window is closed. On OSX, this is the default behaviour.

![](https://cloud.githubusercontent.com/assets/1690993/20858332/9f6508ea-b911-11e6-93b9-3cccca1d663e.png)
![](https://cloud.githubusercontent.com/assets/1690993/20858333/a7a16a1c-b911-11e6-938c-0fe77e2cf405.png)

Options are self-explanatory, enjoy!

Options are self-explained, enjoy!


#### Create new project
### Create new project

Just drag some folders to Sublime Text and then "Add Project". The project files will be created in `Packages/User/Projects/`.

#### Add existing projects to Project Manager
### Add existing projects to Project Manager

There are two ways to add existing projects to Project Manager.
If you want to keep the project files (`.sublime-project` and `sublime-workspace`) in your project directory,
There are two ways to add existing projects to Project Manager. If you want to keep the project files (`*.sublime-project` and `*.sublime-workspace`) in your project directory,

- Open your project file `.sublime-project`, and then use the import option of Project Manager. This tells Project Manager where `.sublime-project` is located and Project Manager will know where to look when the project is opened. In other words, you can put the `.sublime-project` file in any places.
- Open your project file `*.sublime-project`, and then use the import option of Project Manager. This tells Project Manager where `*.sublime-project` is located and Project Manager will know where to look when the project is opened. In other words, you can put the `*.sublime-project` file in any places.

If you want Project Manager manages the project files

- Move your `.sublime-project` and `.sublime-workspace` files in the project directory `Packages/User/Projects/`. You may need to update the project's folder information of the files.

- Move your `*.sublime-project` and `*.sublime-workspace` files in the project directory `Packages/User/Projects/`. You may need to update the project's folder information of the files.

#### Custom Projects directory
### Custom Projects directory

To use a different directory for your projects rather than `Packages/User/Projects/`, edit the following in package settings: Preferences -> Package Settings -> Project Manager
To use a different directory for your projects rather than `Packages/User/Projects/`, edit the following in package settings: `Preferences > Package Settings > Project Manager`

```
```json
{
"projects_path": ["path/to/custom/projects_dir"],
}
```

## Source code

[github.com/randy3k/ProjectManager](https://www.github.com/randy3k/ProjectManager)

### License



+ 0
- 17
closewindow.py View File

@@ -1,17 +0,0 @@
import sublime_plugin


class ProjectManagerCloseWindow(sublime_plugin.WindowCommand):
def run(self):
if self.window.project_file_name():
# if it is a project, close the project
self.window.run_command('close_workspace')
else:
self.window.run_command('close_all')
# exit if there are dirty views
if any([v.is_dirty() for v in self.window.views()]):
return
# close the sidebar
self.window.run_command('close_project')
# close the window
self.window.run_command('close_window')

Default.sublime-commands → commands/Default.sublime-commands View File

@@ -8,7 +8,7 @@
"command": "project_manager", "args": {"action": "add_project"}
},
{
"caption": "Project Manager: Import .sublime-project File",
"caption": "Project Manager: Import *.sublime-project File",
"command": "project_manager", "args": {"action": "import_sublime_project"}
},
{
@@ -42,5 +42,31 @@
{
"caption": "Project Manager: Remove Dead Projects",
"command": "project_manager", "args": {"action": "remove_dead_projects"}
},
{
"caption": "Project Manager: Documentation: Readme",
"command": "pm_readme"
},
{
"caption": "Project Manager: Documentation: Changelog",
"command": "pm_changelog"
},
{
"caption": "Preferences: Project Manager: Settings",
"command": "edit_settings",
"args":
{
"base_file": "${packages}/ProjectManager/settings/project_manager.sublime-settings",
"default": "{\n\t$0\n}\n"
}
},
{
"caption": "Preferences: Project Manager: Key Bindings",
"command": "edit_settings",
"args":
{
"base_file": "${packages}/ProjectManager/input-maps/Default (${platform}).sublime-keymap",
"default": "[\n\t$0\n]\n"
}
}
]

Default → inux).sublime-keymap View File


Default → SX).sublime-keymap View File


Default → indows).sublime-keymap View File


Main.sublime-menu → menus/Main.sublime-menu View File

@@ -1,12 +1,9 @@
[
{
"caption": "Project",
"id": "project",
"children":
[
{
"caption": "-"
},
{ "caption": "-" },
{
"caption": "Project Manager",
"id": "project_manager",
@@ -22,7 +19,7 @@
"command": "project_manager", "args": {"action": "add_project"}
},
{
"caption": "Import .sublime-project File",
"caption": "Import *.sublime-project File",
"command": "project_manager", "args": {"action": "import_sublime_project"}
},
{
@@ -62,14 +59,10 @@
]
},
{
"caption": "Preferences",
"mnemonic": "n",
"id": "preferences",
"children":
[
{
"caption": "Package Settings",
"mnemonic": "P",
"id": "package-settings",
"children":
[
@@ -78,21 +71,42 @@
"children":
[
{
"command": "open_file",
"args": {"file": "${packages}/ProjectManager/project_manager.sublime-settings"},
"caption": "Settings – Default"
"caption": "Documentation",
"children":
[
{
"caption": "Readme",
"command": "pm_readme"
},
{
"caption": "Changelog",
"command": "pm_changelog"
}
]
},
{ "caption": "-" },
{
"command": "open_file",
"args": {"file": "${packages}/User/project_manager.sublime-settings"},
"caption": "Settings – User"
"caption": "Settings",
"command": "edit_settings",
"args":
{
"base_file": "${packages}/ProjectManager/settings/project_manager.sublime-settings",
"default": "{\n\t$0\n}\n"
}
},
{ "caption": "-" }
{
"caption": "Key Bindings",
"command": "edit_settings",
"args":
{
"base_file": "${packages}/ProjectManager/input-maps/Default (${platform}).sublime-keymap",
"default": "[\n\t$0\n]\n"
}
}
]
}
]
}
]
}

]

+ 0
- 528
pm.py View File

@@ -1,528 +0,0 @@
import sublime
import sublime_plugin
import subprocess
import os
import codecs
import platform
import re


def plugin_loaded():
t = sublime.load_settings("Project Manager.sublime-settings")
s = sublime.load_settings("project_manager.sublime-settings")
keys = [
"projects_path",
"use_local_projects_dir",
"show_open_files",
"show_recent_projects_first"
]
d = {}
for k in keys:
if t.has(k) and not s.has(k):
d.update({k: t.get(k)})
for key, value in d.items():
s.set(key, value)
if d:
sublime.save_settings("project_manager.sublime-settings")

old_file = os.path.join(sublime.packages_path(), "User", "Project Manager.sublime-settings")
if os.path.exists(old_file):
os.unlink(old_file)


class JsonFile:
def __init__(self, fpath, encoding="utf-8"):
self.encoding = encoding
self.fpath = fpath

def load(self, default=[]):
self.fdir = os.path.dirname(self.fpath)
if not os.path.isdir(self.fdir):
os.makedirs(self.fdir)
if os.path.exists(self.fpath):
f = codecs.open(self.fpath, "r+", encoding=self.encoding)
content = f.read()
try:
data = sublime.decode_value(content)
except:
sublime.message_dialog("%s is bad!" % self.fpath)
raise
if not data:
data = default
f.close()
else:
f = codecs.open(self.fpath, "w+", encoding=self.encoding)
data = default
f.close()
return data

def save(self, data, indent=4):
self.fdir = os.path.dirname(self.fpath)
if not os.path.isdir(self.fdir):
os.makedirs(self.fdir)
f = codecs.open(self.fpath, "w+", encoding=self.encoding)
f.write(sublime.encode_value(data, True))
f.close()

def remove(self):
if os.path.exists(self.fpath):
os.remove(self.fpath)


def subl(args=[]):
# learnt from SideBarEnhancements
executable_path = sublime.executable_path()
if sublime.platform() == 'osx':
app_path = executable_path[:executable_path.rfind(".app/") + 5]
executable_path = app_path + "Contents/SharedSupport/bin/subl"
subprocess.Popen([executable_path] + args)
if sublime.platform() == "windows":
def fix_focus():
window = sublime.active_window()
view = window.active_view()
window.run_command('focus_neighboring_group')
window.focus_view(view)
sublime.set_timeout(fix_focus, 300)


def expand_folder(folder, project_file):
root = os.path.dirname(project_file)
if not os.path.isabs(folder):
folder = os.path.abspath(os.path.join(root, folder))
return folder


def get_node():
if sublime.platform() == "osx":
node = subprocess.check_output(["scutil", "--get", "ComputerName"]).decode().strip()
else:
node = platform.node().split(".")[0]
return node


def dont_close_windows_when_empty(func):
def f(*args, **kwargs):
preferences = sublime.load_settings("Preferences.sublime-settings")
close_windows_when_empty = preferences.get("close_windows_when_empty")
preferences.set("close_windows_when_empty", False)
func(*args, **kwargs)
if close_windows_when_empty:
preferences.set("close_windows_when_empty", close_windows_when_empty)
return f


class Manager:
def __init__(self, window):
self.window = window
settings_file = 'project_manager.sublime-settings'
self.settings = sublime.load_settings(settings_file)
default_projects_dir = os.path.join(sublime.packages_path(), "User", "Projects")
self.projects_path = self.settings.get(
"projects_path", [self.settings.get("projects_dir", default_projects_dir)])

self.projects_path = [
os.path.normpath(os.path.expanduser(d)) for d in self.projects_path]

node = get_node()
if self.settings.get("use_local_projects_dir", False):
self.projects_path = \
[d + " - " + node for d in self.projects_path] + self.projects_path

self.primary_dir = self.projects_path[0]
self.projects_info = self.get_all_projects_info()

def list_project_files(self, folder):
pfiles = []
library = os.path.join(folder, "library.json")
if os.path.exists(library):
j = JsonFile(library)
for f in j.load([]):
if os.path.exists(f) and f not in pfiles:
pfiles.append(os.path.normpath(f))
pfiles.sort()
j.save(pfiles)
for path, dirs, files in os.walk(folder, followlinks=True):
for f in files:
f = os.path.join(path, f)
if f.endswith(".sublime-project") and f not in pfiles:
pfiles.append(os.path.normpath(f))
# remove empty directories
for d in dirs:
d = os.path.join(path, d)
if len(os.listdir(d)) == 0:
os.rmdir(d)
return pfiles

def get_info_from_project_file(self, pfile):
pdir = self.which_project_dir(pfile)
if pdir:
pname = re.sub("\.sublime-project$", "", os.path.relpath(pfile, pdir))
else:
pname = re.sub("\.sublime-project$", "", os.path.basename(pfile))
pd = JsonFile(pfile).load()
if pd and "folders" in pd and pd["folders"]:
folder = pd["folders"][0].get("path", "")
else:
folder = ""
star = False
for w in sublime.windows():
if w.project_file_name() == pfile:
star = True
break
return {
pname: {
"folder": expand_folder(folder, pfile),
"file": pfile,
"star": star
}
}

def get_all_projects_info(self):
ret = {}
for pdir in self.projects_path:
pfiles = self.list_project_files(pdir)
for f in pfiles:
ret.update(self.get_info_from_project_file(f))
return ret

def which_project_dir(self, pfile):
for pdir in self.projects_path:
if (os.path.realpath(os.path.dirname(pfile))+os.path.sep).startswith(
os.path.realpath(pdir)+os.path.sep):
return pdir
return None

def display_projects(self):
plist = [[key, key + "*" if value["star"] else key, value["folder"], value["file"]]
for key, value in self.projects_info.items()]
plist = sorted(plist)
if self.settings.get("show_recent_projects_first", True):
j = JsonFile(os.path.join(self.primary_dir, "recent.json"))
recent = j.load([])
plist = sorted(plist, key=lambda p: recent.index(p[3]) if p[3] in recent else -1,
reverse=True)

count = 0
for i in range(len(plist)):
if plist[i][0] is not plist[i][1]:
plist.insert(count, plist.pop(i))
count = count + 1
return [item[0] for item in plist], [[item[1], item[2]] for item in plist]

def project_file_name(self, project):
return self.projects_info[project]["file"]

def project_workspace(self, project):
return re.sub("\.sublime-project$", ".sublime-workspace", self.project_file_name(project))

def update_recent(self, project):
j = JsonFile(os.path.join(self.primary_dir, "recent.json"))
recent = j.load([])
pname = self.project_file_name(project)
if pname not in recent:
recent.append(pname)
else:
recent.append(recent.pop(recent.index(pname)))
# only keep the most recent 50 records
if len(recent) > 50:
recent = recent[(50-len(recent)):len(recent)]
j.save(recent)

def clear_recent_projects(self):
def clear_callback():
ok = sublime.ok_cancel_dialog("Clear Recent Projects?")
if ok:
j = JsonFile(os.path.join(self.primary_dir, "recent.json"))
j.remove()

sublime.set_timeout(clear_callback, 100)

def get_project_data(self, project):
return JsonFile(self.project_file_name(project)).load()

def check_project(self, project):
wsfile = self.project_workspace(project)
j = JsonFile(wsfile)
if not os.path.exists(wsfile):
j.save({})
elif self.settings.has("show_open_files"):
show_open_files = self.settings.get("show_open_files", False)
data = j.load({})
data["show_open_files"] = show_open_files
df = data.get("distraction_free", {})
df["show_open_files"] = show_open_files
data["distraction_free"] = df
j.save(data)

@dont_close_windows_when_empty
def close_project_by_window(self, window):
window.run_command("close_workspace")

def close_project_by_name(self, project):
pfile = os.path.realpath(self.project_file_name(project))
for w in sublime.windows():
if w.project_file_name():
if os.path.realpath(w.project_file_name()) == pfile:
self.close_project_by_window(w)
if w.id() != sublime.active_window().id():
w.run_command("close_window")
return True
return False

def add_project(self):
@dont_close_windows_when_empty
def close_all_files():
self.window.run_command("close_all")

def add_callback(project):
pd = self.window.project_data()
f = os.path.join(self.primary_dir, "%s.sublime-project" % project)
if pd:
JsonFile(f).save(pd)
else:
JsonFile(f).save({})
JsonFile(re.sub("\.sublime-project$", ".sublime-workspace", f)).save({})
self.close_project_by_window(self.window)
self.window.run_command("close_project")
close_all_files()

# reload projects info
self.__init__(self.window)
self.switch_project(project)

def show_input_panel():
project = "New Project"
pd = self.window.project_data()
pf = self.window.project_file_name()
try:
path = pd["folders"][0]["path"]
if pf:
project = os.path.basename(expand_folder(path, pf))
else:
project = os.path.basename(path)
except:
pass

v = self.window.show_input_panel("Project name:", project, add_callback, None, None)
v.run_command("select_all")

sublime.set_timeout(show_input_panel, 100)

def import_sublime_project(self):
pfile = self.window.project_file_name()
if not pfile:
sublime.message_dialog("Project file not found!")
return
if self.which_project_dir(pfile):
sublime.message_dialog("This project was created by Project Manager!")
return
ok = sublime.ok_cancel_dialog("Import %s?" % os.path.basename(pfile))
if ok:
j = JsonFile(os.path.join(self.primary_dir, "library.json"))
data = j.load([])
if pfile not in data:
data.append(pfile)
j.save(data)

def append_project(self, project):
self.update_recent(project)
pd = self.get_project_data(project)
paths = [expand_folder(f.get("path"), self.project_file_name(project))
for f in pd.get("folders")]
subl(["-a"] + paths)

def switch_project(self, project):
self.update_recent(project)
self.check_project(project)
self.close_project_by_window(self.window)
self.close_project_by_name(project)
subl([self.project_file_name(project)])

def open_in_new_window(self, project):
self.update_recent(project)
self.check_project(project)
self.close_project_by_name(project)
subl(["-n", self.project_file_name(project)])

def _remove_project(self, project):
ok = sublime.ok_cancel_dialog("Remove \"%s\" from Project Manager?" % project)
if ok:
pfile = self.project_file_name(project)
if self.which_project_dir(pfile):
self.close_project_by_name(project)
os.unlink(self.project_file_name(project))
os.unlink(self.project_workspace(project))
else:
for pdir in self.projects_path:
j = JsonFile(os.path.join(pdir, "library.json"))
data = j.load([])
if pfile in data:
data.remove(pfile)
j.save(data)
sublime.status_message("Project \"%s\" is removed." % project)

def remove_project(self, project):
sublime.set_timeout(lambda: self._remove_project(project), 100)

def clean_dead_projects(self):
projects_to_remove = []
for pname, pi in self.projects_info.items():
folder = pi["folder"]
if not os.path.exists(folder):
projects_to_remove.append(pname)

def remove_projects_iteratively():
pname = projects_to_remove[0]
self._remove_project(pname)
projects_to_remove.remove(pname)
if len(projects_to_remove) > 0:
sublime.set_timeout(remove_projects_iteratively, 100)

if len(projects_to_remove) > 0:
sublime.set_timeout(remove_projects_iteratively, 100)
else:
sublime.message_dialog("No Dead Projects.")

def edit_project(self, project):
def on_open():
self.window.open_file(self.project_file_name(project))
sublime.set_timeout_async(on_open, 100)

def rename_project(self, project):
def rename_callback(new_project):
if project == new_project:
return
pfile = self.project_file_name(project)
wsfile = self.project_workspace(project)
pdir = self.which_project_dir(pfile)
if not pdir:
pdir = os.path.dirname(pfile)
new_pfile = os.path.join(pdir, "%s.sublime-project" % new_project)
new_wsfile = re.sub("\.sublime-project$", ".sublime-workspace", new_pfile)

reopen = self.close_project_by_name(project)
os.rename(pfile, new_pfile)
os.rename(wsfile, new_wsfile)

j = JsonFile(new_wsfile)
data = j.load({})
if "project" in data:
data["project"] = "%s.sublime-project" % os.path.basename(new_project)
j.save(data)

if not self.which_project_dir(pfile):
for pdir in self.projects_path:
library = os.path.join(pdir, "library.json")
if os.path.exists(library):
j = JsonFile(library)
data = j.load([])
if pfile in data:
data.remove(pfile)
data.append(new_pfile)
j.save(data)

if reopen:
# reload projects info
self.__init__(self.window)
self.open_in_new_window(new_project)

def show_input_panel():
v = self.window.show_input_panel("New project name:",
project, rename_callback, None, None)
v.run_command("select_all")

sublime.set_timeout(show_input_panel, 100)


def cancellable(func):
def _ret(self, action):
if action >= 0:
func(self, action)
elif action < 0 and self.caller == "manager":
sublime.set_timeout(self.run, 10)
return _ret


class ProjectManager(sublime_plugin.WindowCommand):

def show_quick_panel(self, items, on_done):
sublime.set_timeout(
lambda: self.window.show_quick_panel(items, on_done),
10)

def run(self, action=None, caller=None):
self.manager = Manager(self.window)

if action is None:
self.show_options()
elif action == "add_project":
self.manager.add_project()
elif action == "import_sublime_project":
self.manager.import_sublime_project()
elif action == "clear_recent_projects":
self.manager.clear_recent_projects()
elif action == "remove_dead_projects":
self.manager.clean_dead_projects()
else:
self.caller = caller
callback = eval("self.on_" + action)
self.projects, display = self.manager.display_projects()
if not self.projects:
sublime.message_dialog("Project list is empty.")
return
self.show_quick_panel(display, callback)

def show_options(self):
items = [
["Open Project", "Open project in the current window"],
["Open Project in New Window", "Open project in a new window"],
["Append Project", "Append project to current window"],
["Edit Project", "Edit project settings"],
['Rename Project', "Rename project"],
["Remove Project", "Remove from Project Manager"],
["Add New Project", "Add current folders to Project Manager"],
["Import Project", "Import current .sublime-project file"],
["Clear Recent Projects", "Clear Recent Projects"],
["Remove Dead Projects", "Remove Dead Projects"]
]

def callback(a):
if a < 0:
return
elif a <= 5:
actions = ["switch", "new", "append", "edit", "rename", "remove"]
self.run(action=actions[a], caller="manager")
elif a == 6:
self.run(action="add_project")
elif a == 7:
self.run(action="import_sublime_project")
elif a == 8:
self.run(action="clear_recent_projects")
elif a == 9:
self.run(action="remove_dead_projects")

self.show_quick_panel(items, callback)

@cancellable
def on_new(self, action):
self.manager.open_in_new_window(self.projects[action])

@cancellable
def on_switch(self, action):
self.manager.switch_project(self.projects[action])

@cancellable
def on_append(self, action):
self.manager.append_project(self.projects[action])

@cancellable
def on_remove(self, action):
self.manager.remove_project(self.projects[action])

@cancellable
def on_edit(self, action):
self.manager.edit_project(self.projects[action])

@cancellable
def on_rename(self, action):
self.manager.rename_project(self.projects[action])

+ 5
- 0
project_manager.py View File

@@ -0,0 +1,5 @@
#!/usr/bin/env python
# coding: utf-8


from .src import *

project_manager.sublime-settings → settings/project_manager.sublime-settings View File


+ 1
- 0
src/__init__.py View File

@@ -0,0 +1 @@
__pkg_name__ = 'ProjectManager'

+ 25
- 0
src/fix_old_settings_file.py View File

@@ -0,0 +1,25 @@
import sublime
import sublime_plugin
import os
def plugin_loaded():
t = sublime.load_settings('Project Manager.sublime-settings')
s = sublime.load_settings('project_manager.sublime-settings')
keys = [
'projects_path',
'use_local_projects_dir',
'show_open_files',
'show_recent_projects_first'
]
d = {}
for k in keys:
if t.has(k) and not s.has(k):
d.update({k: t.get(k)})
for key, value in d.items():
s.set(key, value)
if d:
sublime.save_settings('project_manager.sublime-settings')
old_file = os.path.join(sublime.packages_path(), 'User', 'Project Manager.sublime-settings')
if os.path.exists(old_file):
os.remove(old_file)

+ 38
- 0
src/json_file.py View File

@@ -0,0 +1,38 @@
import sublime
import os
class JsonFile:
def __init__(self, fpath, encoding='utf-8'):
self.encoding = encoding
self.fpath = fpath
def load(self, default=[]):
self.fdir = os.path.dirname(self.fpath)
if not os.path.isdir(self.fdir):
os.makedirs(self.fdir)
if os.path.exists(self.fpath):
with open(self.fpath, mode='r', encoding=self.encoding) as f:
content = f.read()
try:
data = sublime.decode_value(content)
except:
sublime.message_dialog('%s is bad!' % self.fpath)
raise
if not data:
data = default
else:
with open(self.fpath, mode='w', encoding=self.encoding, newline='\n') as f:
data = default
f.write(data)
return data
def save(self, data, indent=4):
self.fdir = os.path.dirname(self.fpath)
if not os.path.isdir(self.fdir):
os.makedirs(self.fdir)
with open(self.fpath, mode='w', encoding=self.encoding, newline='\n') as f:
f.write(sublime.encode_value(data, True))
def remove(self):
if os.path.exists(self.fpath):
os.remove(self.fpath)

+ 390
- 0
src/project_manager.py View File

@@ -0,0 +1,390 @@
import sublime
import sublime_plugin
import subprocess
import os
import platform
import re

from .json_file import JsonFile


def subl(args=[]):
# learnt from SideBarEnhancements
executable_path = sublime.executable_path()
if sublime.platform() == 'osx':
app_path = executable_path[:executable_path.rfind('.app/') + 5]
executable_path = app_path + 'Contents/SharedSupport/bin/subl'
subprocess.Popen([executable_path] + args)
if sublime.platform() == 'windows':
def fix_focus():
window = sublime.active_window()
view = window.active_view()
window.run_command('focus_neighboring_group')
window.focus_view(view)
sublime.set_timeout(fix_focus,
300)


def expand_folder(folder, project_file):
root = os.path.dirname(project_file)
if not os.path.isabs(folder):
folder = os.path.abspath(os.path.join(root, folder))
return folder


def get_node():
if sublime.platform() == 'osx':
node = subprocess.check_output(['scutil', '--get', 'ComputerName']).decode().strip()
else:
node = platform.node().split('.')[0]
return node


def dont_close_windows_when_empty(func):
def f(*args, **kwargs):
s = sublime.load_settings('Preferences.sublime-settings')
close_windows_when_empty = s.get('close_windows_when_empty')
s.set('close_windows_when_empty', False)
func(*args, **kwargs)
if close_windows_when_empty:
s.set('close_windows_when_empty', close_windows_when_empty)
return f


class Manager:
def __init__(self, window):
self.window = window
s = 'project_manager.sublime-settings'
self.settings = sublime.load_settings(s)
default_projects_dir = os.path.join(sublime.packages_path(),
'User',
'Projects')
self.projects_path = self.settings.get(
'projects_path', [self.settings.get('projects_dir', default_projects_dir)])

self.projects_path = [
os.path.normpath(os.path.expanduser(d)) for d in self.projects_path]

node = get_node()
if self.settings.get('use_local_projects_dir', False):
self.projects_path = \
[d + ' - ' + node for d in self.projects_path] + self.projects_path

self.primary_dir = self.projects_path[0]
self.projects_info = self.get_all_projects_info()

def list_project_files(self, folder):
pfiles = []
library = os.path.join(folder, 'library.json')
if os.path.exists(library):
j = JsonFile(library)
for f in j.load([]):
if os.path.exists(f) and f not in pfiles:
pfiles.append(os.path.normpath(f))
pfiles.sort()
j.save(pfiles)
for path, dirs, files in os.walk(folder, followlinks=True):
for f in files:
f = os.path.join(path, f)
if f.endswith('.sublime-project') and f not in pfiles:
pfiles.append(os.path.normpath(f))
# remove empty directories
for d in dirs:
d = os.path.join(path, d)
if len(os.listdir(d)) == 0:
os.rmdir(d)
return pfiles

def get_info_from_project_file(self, pfile):
pdir = self.which_project_dir(pfile)
if pdir:
pname = re.sub('\.sublime-project$',
'',
os.path.relpath(pfile, pdir))
else:
pname = re.sub('\.sublime-project$',
'',
os.path.basename(pfile))
pd = JsonFile(pfile).load()
if pd and 'folders' in pd and pd['folders']:
folder = pd['folders'][0].get('path', '')
else:
folder = ''
star = False
for w in sublime.windows():
if w.project_file_name() == pfile:
star = True
break
return {
pname: {
'folder': expand_folder(folder, pfile),
'file': pfile,
'star': star
}
}

def get_all_projects_info(self):
ret = {}
for pdir in self.projects_path:
pfiles = self.list_project_files(pdir)
for f in pfiles:
ret.update(self.get_info_from_project_file(f))
return ret

def which_project_dir(self, pfile):
for pdir in self.projects_path:
if (os.path.realpath(os.path.dirname(pfile))+os.path.sep).startswith(
os.path.realpath(pdir)+os.path.sep):
return pdir
return None

def display_projects(self):
plist = [[key, key + '*' if value['star'] else key, value['folder'], value['file']]
for key, value in self.projects_info.items()]
plist = sorted(plist)
if self.settings.get('show_recent_projects_first', True):
j = JsonFile(os.path.join(self.primary_dir, 'recent.json'))
recent = j.load([])
plist = sorted(plist,
key=lambda p: recent.index(p[3]) if p[3] in recent else -1,
reverse=True)

count = 0
for i in range(len(plist)):
if plist[i][0] is not plist[i][1]:
plist.insert(count, plist.pop(i))
count = count + 1
return [item[0] for item in plist], [[item[1], item[2]] for item in plist]

def project_file_name(self, project):
return self.projects_info[project]['file']

def project_workspace(self, project):
return re.sub('\.sublime-project$',
'.sublime-workspace',
self.project_file_name(project))

def update_recent(self, project):
j = JsonFile(os.path.join(self.primary_dir, 'recent.json'))
recent = j.load([])
pname = self.project_file_name(project)
if pname not in recent:
recent.append(pname)
else:
recent.append(recent.pop(recent.index(pname)))
# only keep the most recent 50 records
if len(recent) > 50:
recent = recent[(50-len(recent)):len(recent)]
j.save(recent)

def clear_recent_projects(self):
def clear_callback():
answer = sublime.ok_cancel_dialog('Clear Recent Projects?')
if answer is True:
j = JsonFile(os.path.join(self.primary_dir, 'recent.json'))
j.remove()

sublime.set_timeout(clear_callback, 100)

def get_project_data(self, project):
return JsonFile(self.project_file_name(project)).load()

def check_project(self, project):
wsfile = self.project_workspace(project)
j = JsonFile(wsfile)
if not os.path.exists(wsfile):
j.save({})
elif self.settings.has('show_open_files'):
show_open_files = self.settings.get('show_open_files', False)
data = j.load({})
data['show_open_files'] = show_open_files
df = data.get('distraction_free', {})
df['show_open_files'] = show_open_files
data['distraction_free'] = df
j.save(data)

@dont_close_windows_when_empty
def close_project_by_window(self, window):
window.run_command('close_workspace')

def close_project_by_name(self, project):
pfile = os.path.realpath(self.project_file_name(project))
for w in sublime.windows():
if w.project_file_name():
if os.path.realpath(w.project_file_name()) == pfile:
self.close_project_by_window(w)
if w.id() != sublime.active_window().id():
w.run_command('close_window')
return True
return False

def add_project(self):
@dont_close_windows_when_empty
def close_all_files():
self.window.run_command('close_all')

def add_callback(project):
pd = self.window.project_data()
f = os.path.join(self.primary_dir, '%s.sublime-project' % project)
if pd:
JsonFile(f).save(pd)
else:
JsonFile(f).save({})
JsonFile(re.sub('\.sublime-project$', '.sublime-workspace', f)).save({})
self.close_project_by_window(self.window)
self.window.run_command('close_project')
close_all_files()

# reload projects info
self.__init__(self.window)
self.switch_project(project)

def show_input_panel():
project = 'New Project'
pd = self.window.project_data()
pf = self.window.project_file_name()
try:
path = pd['folders'][0]['path']
if pf:
project = os.path.basename(expand_folder(path, pf))
else:
project = os.path.basename(path)
except:
pass

v = self.window.show_input_panel('Project name:',
project,
add_callback,
None,
None)
v.run_command('select_all')

sublime.set_timeout(show_input_panel, 100)

def import_sublime_project(self):
pfile = self.window.project_file_name()
if not pfile:
sublime.message_dialog('Project file not found!')
return
if self.which_project_dir(pfile):
sublime.message_dialog('This project was created by Project Manager!')
return
answer = sublime.ok_cancel_dialog('Import %s?' % os.path.basename(pfile))
if answer is True:
j = JsonFile(os.path.join(self.primary_dir, 'library.json'))
data = j.load([])
if pfile not in data:
data.append(pfile)
j.save(data)

def append_project(self, project):
self.update_recent(project)
pd = self.get_project_data(project)
paths = [expand_folder(f.get('path'), self.project_file_name(project))
for f in pd.get('folders')]
subl(['-a'] + paths)

def switch_project(self, project):
self.update_recent(project)
self.check_project(project)
self.close_project_by_window(self.window)
self.close_project_by_name(project)
subl([self.project_file_name(project)])

def open_in_new_window(self, project):
self.update_recent(project)
self.check_project(project)
self.close_project_by_name(project)
subl(['-n', self.project_file_name(project)])

def _remove_project(self, project):
answer = sublime.ok_cancel_dialog('Remove "%s" from Project Manager?' % project)
if answer is True:
pfile = self.project_file_name(project)
if self.which_project_dir(pfile):
self.close_project_by_name(project)
os.remove(self.project_file_name(project))
os.remove(self.project_workspace(project))
else:
for pdir in self.projects_path:
j = JsonFile(os.path.join(pdir, 'library.json'))
data = j.load([])
if pfile in data:
data.remove(pfile)
j.save(data)
sublime.status_message('Project "%s" is removed.' % project)

def remove_project(self, project):
sublime.set_timeout(lambda: self._remove_project(project), 100)

def clean_dead_projects(self):
projects_to_remove = []
for pname, pi in self.projects_info.items():
folder = pi['folder']
if not os.path.exists(folder):
projects_to_remove.append(pname)

def remove_projects_iteratively():
pname = projects_to_remove[0]
self._remove_project(pname)
projects_to_remove.remove(pname)
if len(projects_to_remove) > 0:
sublime.set_timeout(remove_projects_iteratively, 100)

if len(projects_to_remove) > 0:
sublime.set_timeout(remove_projects_iteratively, 100)
else:
sublime.message_dialog('No Dead Projects.')

def edit_project(self, project):
def on_open():
self.window.open_file(self.project_file_name(project))
sublime.set_timeout_async(on_open, 100)

def rename_project(self, project):
def rename_callback(new_project):
if project == new_project:
return
pfile = self.project_file_name(project)
wsfile = self.project_workspace(project)
pdir = self.which_project_dir(pfile)
if not pdir:
pdir = os.path.dirname(pfile)
new_pfile = os.path.join(pdir, '%s.sublime-project' % new_project)
new_wsfile = re.sub('\.sublime-project$', '.sublime-workspace', new_pfile)

reopen = self.close_project_by_name(project)
os.rename(pfile, new_pfile)
os.rename(wsfile, new_wsfile)

j = JsonFile(new_wsfile)
data = j.load({})
if 'project' in data:
data['project'] = '%s.sublime-project' % os.path.basename(new_project)
j.save(data)

if not self.which_project_dir(pfile):
for pdir in self.projects_path:
library = os.path.join(pdir, 'library.json')
if os.path.exists(library):
j = JsonFile(library)
data = j.load([])
if pfile in data:
data.remove(pfile)
data.append(new_pfile)
j.save(data)

if reopen:
# reload projects info
self.__init__(self.window)
self.open_in_new_window(new_project)

def show_input_panel():
v = self.window.show_input_panel('New project name:',
project,
rename_callback,
None,
None)
v.run_command('select_all')

sublime.set_timeout(show_input_panel, 100)

+ 28
- 0
src/text_commands.py View File

@@ -0,0 +1,28 @@
import sublime
import sublime_plugin
from . import __pkg_name__
class PmReadmeCommand(sublime_plugin.TextCommand):
def run(self, edit):
v = self.view.window().new_file()
v.set_name(__pkg_name__ + ': Readme')
v.settings().set('gutter', False)
v.insert(edit, 0, sublime.load_resource('Packages/' + __pkg_name__ + '/README.md'))
v.set_syntax_file('Packages/Markdown/Markdown.sublime-syntax')
v.set_read_only(True)
v.set_scratch(True)
class PmChangelogCommand(sublime_plugin.TextCommand):
def run(self, edit):
v = self.view.window().new_file()
v.set_name(__pkg_name__ + ': Changelog')
v.settings().set('gutter', False)
v.insert(edit, 0, sublime.load_resource('Packages/' + __pkg_name__ + '/CHANGELOG.md'))
v.set_syntax_file('Packages/Markdown/Markdown.sublime-syntax')
v.set_read_only(True)
v.set_scratch(True)

+ 112
- 0
src/window_commands.py View File

@@ -0,0 +1,112 @@
import sublime
import sublime_plugin
from .project_manager import Manager

def cancellable(func):
def _ret(self, action):
if action >= 0:
func(self, action)
elif action < 0 and self.caller == 'manager':
sublime.set_timeout(self.run, 10)
return _ret


class ProjectManagerCloseWindow(sublime_plugin.WindowCommand):
def run(self):
if self.window.project_file_name():
# if it is a project, close the project
self.window.run_command('close_workspace')
else:
self.window.run_command('close_all')
# exit if there are dirty views
if any([v.is_dirty() for v in self.window.views()]):
return
# close the sidebar
self.window.run_command('close_project')
# close the window
self.window.run_command('close_window')


class ProjectManager(sublime_plugin.WindowCommand):

def show_quick_panel(self, items, on_done):
sublime.set_timeout(
lambda: self.window.show_quick_panel(items, on_done),
10)

def run(self, action=None, caller=None):
self.manager = Manager(self.window)

if action is None:
self.show_options()
elif action == 'add_project':
self.manager.add_project()
elif action == 'import_sublime_project':
self.manager.import_sublime_project()
elif action == 'clear_recent_projects':
self.manager.clear_recent_projects()
elif action == 'remove_dead_projects':
self.manager.clean_dead_projects()
else:
self.caller = caller
callback = eval('self.on_' + action)
self.projects, display = self.manager.display_projects()
if not self.projects:
sublime.message_dialog('Project list is empty.')
return
self.show_quick_panel(display, callback)

def show_options(self):
items = [
['Open Project', 'Open project in the current window'],
['Open Project in New Window', 'Open project in a new window'],
['Append Project', 'Append project to current window'],
['Edit Project', 'Edit project settings'],
['Rename Project', 'Rename project'],
['Remove Project', 'Remove from Project Manager'],
['Add New Project', 'Add current folders to Project Manager'],
['Import Project', 'Import current .sublime-project file'],
['Clear Recent Projects', 'Clear Recent Projects'],
['Remove Dead Projects', 'Remove Dead Projects']
]

def callback(a):
if a < 0:
return
elif a <= 5:
actions = ['switch', 'new', 'append', 'edit', 'rename', 'remove']
self.run(action=actions[a], caller='manager')
elif a == 6:
self.run(action='add_project')
elif a == 7:
self.run(action='import_sublime_project')
elif a == 8:
self.run(action='clear_recent_projects')
elif a == 9:
self.run(action='remove_dead_projects')

self.show_quick_panel(items, callback)

@cancellable
def on_new(self, action):
self.manager.open_in_new_window(self.projects[action])

@cancellable
def on_switch(self, action):
self.manager.switch_project(self.projects[action])

@cancellable
def on_append(self, action):
self.manager.append_project(self.projects[action])

@cancellable
def on_remove(self, action):
self.manager.remove_project(self.projects[action])

@cancellable
def on_edit(self, action):
self.manager.edit_project(self.projects[action])

@cancellable
def on_rename(self, action):
self.manager.rename_project(self.projects[action])

Loading…
Cancel
Save