From 005ae4755aa1b26aa6cf33b3d3278d295aed6112 Mon Sep 17 00:00:00 2001 From: redatman Date: Thu, 1 Aug 2024 21:19:01 +0800 Subject: [PATCH 1/8] docs: Clarify package install locations for different OSes Improve the clarity of package installation paths by adding alternative locations for each OS. This ensures users can more easily locate the correct directory for installing packages. --- README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d1be67b..ad89bc2 100644 --- a/README.md +++ b/README.md @@ -55,19 +55,25 @@ The "Packages" directory is located at: * OS X: ```bash -~/Library/Application Support/Sublime Text/Packages +~/Library/Application Support/Sublime Text/Packages/ +# or +~/Library/Application Support/Sublime Text/Installed Packages/ ``` * Linux: ```bash ~/.config/sublime-text/Packages/ +# or +~/.config/sublime-text/Installed Packages/ ``` * Windows: ```bash %APPDATA%/Sublime Text/Packages/ +# or +%APPDATA%/Sublime Text/Installed Packages/ ``` ## Configuration From 6519a8d479ddadad294d50d6b1ecbb460f17fdca Mon Sep 17 00:00:00 2001 From: redatman Date: Thu, 1 Aug 2024 21:19:22 +0800 Subject: [PATCH 2/8] misc: Add local development deploy script Adds a script to simplify deploying the plugin locally for development purposes. This script creates a Sublime Text package and moves it to the appropriate location in the user's Sublime Text Packages directory. --- dev_deploy.sh | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 dev_deploy.sh diff --git a/dev_deploy.sh b/dev_deploy.sh new file mode 100644 index 0000000..47ddef6 --- /dev/null +++ b/dev_deploy.sh @@ -0,0 +1,4 @@ +# local development deploy +zip -r Simplenote.sublime-package . -x ".git/*" ".github/*" ".gitignore" ".idea/*" ".vscode/*" ".pytest_cache/*" "pyproject.toml" "package-lock.json" "package.json" "node_modules/*" ".env.*" ".DS_Store" "assets/*" "*__pycache__/*" "tmp/*" "tests/*" "logs/*" "sublime_api.py" "dev_deploy.sh" "package-metadata.json" + +mv Simplenote.sublime-package $HOME/Library/Application\ Support/Sublime\ Text/Installed\ Packages/Simplenote.sublime-package From e44b613270fd3dcbabc8efcbd32c543867682ab4 Mon Sep 17 00:00:00 2001 From: redatman Date: Fri, 2 Aug 2024 06:29:34 +0800 Subject: [PATCH 3/8] feat: show quick panel after first sync Refactored the note listing and synchronization process to enhance user experience. - Simplified the quick panel display for notes, providing a cleaner interface. - Improved note synchronization by handling first-sync scenarios and incorporating a callback mechanism for post-sync actions. - Introduced a "sync_note_number" setting to control the number of notes fetched during synchronization, enhancing efficiency. This change streamlines the note management process, providing a more intuitive and efficient workflow for users. --- commands.py | 55 +++++++++-------------------------------------------- lib/core.py | 10 +++++----- lib/gui.py | 51 +++++++++++++++++++++++++++++++++++++++++++++++++ main.py | 25 ++++++++++++------------ 4 files changed, 77 insertions(+), 64 deletions(-) diff --git a/commands.py b/commands.py index bd19f36..f184ad7 100644 --- a/commands.py +++ b/commands.py @@ -8,7 +8,7 @@ from ._config import CONFIG from .lib.core import start -from .lib.gui import clear_orphaned_filepaths, close_view, on_note_changed, open_view, show_message +from .lib.gui import close_view, on_note_changed, open_view, show_message, show_quick_panel from .lib.models import Note from .lib.operations import NoteCreator, NoteDeleter, NotesIndicator, NoteUpdater, OperationManager @@ -34,7 +34,6 @@ class SimplenoteViewCommand(sublime_plugin.EventListener): @cached_property def autosave_debounce_time(self) -> int: - logger.warning(("CONFIG.SIMPLENOTE_SETTINGS_FILE_PATH", CONFIG.SIMPLENOTE_SETTINGS_FILE_PATH)) settings = sublime.load_settings(CONFIG.SIMPLENOTE_SETTINGS_FILE_PATH) _autosave_debounce_time = settings.get("autosave_debounce_time", 1) if not isinstance(_autosave_debounce_time, int): @@ -123,53 +122,12 @@ def on_post_save(self, view: sublime.View): class SimplenoteListCommand(sublime_plugin.ApplicationCommand): - def on_select(self, selected_index: int): - if selected_index == -1: - return - note_id = self.list__modificationDate[selected_index] - selected_note = Note.tree.find(note_id) - if not isinstance(selected_note, Note): - show_message("Note not found: note id(%s), Please restart simplenote or sublime text." % note_id) - return - filepath = selected_note.open() - selected_note.flush() - view = open_view(filepath) - def run(self): global SIMPLENOTE_STARTED if not SIMPLENOTE_STARTED: if not start(): return - - if Note.tree.count <= 0: - show_message( - "No notes found. Please wait for the synchronization to complete, or press [super+shift+s, super+shift+c] to create a note." - ) - self.list__modificationDate: List[float] = [] - self.list__title: List[str] = [] - list__filename: List[str] = [] - for note in Note.tree.iter(reverse=True): - if not isinstance(note, Note): - raise Exception("note is not a Note: %s" % type(note)) - if note.d.deleted == True: - continue - self.list__modificationDate.append(note.d.modificationDate) - self.list__title.append(note.title) - list__filename.append(note.filename) - - # TODO: Maybe doesn't need to run every time - clear_orphaned_filepaths(list__filename) - - def show_panel(): - sublime.active_window().show_quick_panel( - self.list__title, - self.on_select, - flags=sublime.KEEP_OPEN_ON_FOCUS_LOST, - # on_highlight=self.on_select, - placeholder="Select Note press key 'enter' to open", - ) - - sublime.set_timeout(show_panel, 50) + show_quick_panel() class SimplenoteSyncCommand(sublime_plugin.ApplicationCommand): @@ -179,14 +137,19 @@ def merge_note(self, updated_notes: List[Note]): if note.need_flush: on_note_changed(note) - def run(self): + def callback(self, updated_notes: List[Note], first_sync: bool = False): + self.merge_note(updated_notes) + if first_sync: + show_quick_panel(first_sync) + + def run(self, first_sync: bool = False): settings = sublime.load_settings(CONFIG.SIMPLENOTE_SETTINGS_FILE_PATH) sync_note_number = settings.get("sync_note_number", 1000) if not isinstance(sync_note_number, int): show_message("`sync_note_number` must be an integer. Please check settings file.") return note_indicator = NotesIndicator(sync_note_number=sync_note_number) - note_indicator.set_callback(self.merge_note) + note_indicator.set_callback(self.callback, {"first_sync": first_sync}) OperationManager().add_operation(note_indicator) diff --git a/lib/core.py b/lib/core.py index 026e918..37c4aee 100644 --- a/lib/core.py +++ b/lib/core.py @@ -9,12 +9,12 @@ logger = logging.getLogger() SIMPLENOTE_STARTED = False +manager = OperationManager() -def sync(): - manager = OperationManager() +def sync(first_sync: bool = False): if not manager.running: - sublime.run_command("simplenote_sync") + sublime.run_command("simplenote_sync", {"first_sync": first_sync}) else: logger.debug("Sync omitted") @@ -36,11 +36,11 @@ def start(): password = settings.get("password") if username and password: - sync() + sync(first_sync=True) SIMPLENOTE_STARTED = True else: edit_settings() - show_message("Simplenote: Please configure username/password, Please check settings file.") + show_message("Simplenote: Please configure username/password in settings file.") sublime.set_timeout(remove_status, 2000) SIMPLENOTE_STARTED = False return SIMPLENOTE_STARTED diff --git a/lib/gui.py b/lib/gui.py index c33e1ef..e512010 100644 --- a/lib/gui.py +++ b/lib/gui.py @@ -21,6 +21,7 @@ "close_view", "clear_orphaned_filepaths", "on_note_changed", + "show_quick_panel", ] @@ -121,3 +122,53 @@ def on_note_changed(note: Note): old_window.focus_view(old_active_view) sublime.set_timeout(partial(new_view.run_command, "revert"), 0) + + +def on_select(list__modificationDate: List[float], selected_index: int): + if selected_index == -1: + return + note_id = list__modificationDate[selected_index] + selected_note = Note.tree.find(note_id) + if not isinstance(selected_note, Note): + show_message("Note not found: note id(%s), Please restart simplenote or sublime text." % note_id) + return + filepath = selected_note.open() + selected_note.flush() + view = open_view(filepath) + + +def show_quick_panel(first_sync: bool = False): + logger.warning("show_quick_panel") + if Note.tree.count <= 0: + show_message( + "No notes found. Please wait for the synchronization to complete, or press [super+shift+s, super+shift+c] to create a note." + ) + list__modificationDate: List[float] = [] + list__title: List[str] = [] + list__filename: List[str] = [] + for note in Note.tree.iter(reverse=True): + if not isinstance(note, Note): + raise Exception("note is not a Note: %s" % type(note)) + if note.d.deleted == True: + continue + list__modificationDate.append(note.d.modificationDate) + list__title.append(note.title) + list__filename.append(note.filename) + + # TODO: Maybe doesn't need to run every time + clear_orphaned_filepaths(list__filename) + + placeholder = "Select Note press key 'enter' to open" + if first_sync: + placeholder = "Sync complete. Press [super+shift+s] [super+shift+l] to display the note list again." + + def show_panel(): + sublime.active_window().show_quick_panel( + list__title, + partial(on_select, list__modificationDate), + flags=sublime.MONOSPACE_FONT, + # on_highlight=self.on_select, + placeholder=placeholder, + ) + + sublime.set_timeout(show_panel, 500) diff --git a/main.py b/main.py index dc3f65f..c1854d1 100644 --- a/main.py +++ b/main.py @@ -1,9 +1,9 @@ +import functools import logging import sublime -from ._config import CONFIG -from .lib.core import start +from .lib.core import start, sync from .lib.models import Note @@ -12,7 +12,7 @@ SIMPLENOTE_RELOAD_CALLS = -1 -def reload_if_needed(): +def reload_if_needed(autostart: bool = True): # global SIMPLENOTE_RELOAD_CALLS # # Sublime calls this twice for some reason :( @@ -21,14 +21,10 @@ def reload_if_needed(): # logger.debug("Simplenote Reload call %s" % SIMPLENOTE_RELOAD_CALLS) # return - logger.warning(("CONFIG.SIMPLENOTE_SETTINGS_FILE_PATH", CONFIG.SIMPLENOTE_SETTINGS_FILE_PATH)) - settings = sublime.load_settings("Simplenote.sublime-settings") - autostart = settings.get("autostart") - if bool(autostart): - autostart = True logger.debug(("Simplenote Reloading", autostart)) + start() if autostart: - sublime.set_timeout(start, 2000) + sublime.set_timeout_async(sync, 2000) logger.debug("Auto Starting") @@ -36,13 +32,16 @@ def plugin_loaded(): # load_notes() logger.debug(("Loaded notes number: ", Note.tree.count)) - logger.warning(("CONFIG.SIMPLENOTE_SETTINGS_FILE_PATH", CONFIG.SIMPLENOTE_SETTINGS_FILE_PATH)) settings = sublime.load_settings("Simplenote.sublime-settings") # logger.debug(("SETTINGS.__dict__: ", SETTINGS.__dict__)) # logger.debug(("SETTINGS.username: ", SETTINGS.get("username"))) + autostart = settings.get("autostart", True) + if not isinstance(autostart, bool): + autostart = True + callback = functools.partial(reload_if_needed, autostart) settings.clear_on_change("username") settings.clear_on_change("password") - settings.add_on_change("username", reload_if_needed) - settings.add_on_change("password", reload_if_needed) + settings.add_on_change("username", callback) + settings.add_on_change("password", callback) - reload_if_needed() + reload_if_needed(autostart=autostart) From 301ce35615f44cf8a1b2e10c61ef995041531042 Mon Sep 17 00:00:00 2001 From: redatman Date: Sat, 3 Aug 2024 06:31:53 +0800 Subject: [PATCH 4/8] feat: introduce optimistic locking for global storage This commit introduces optimistic locking for the global storage, which is used to store state information such as the last sync time. This change aims to improve the robustness of the plugin by preventing race conditions that might occur in multi-threaded environments, particularly when syncing notes. The optimistic locking mechanism ensures that updates to the global storage are performed in a safe and atomic manner, mitigating potential conflicts that could arise from concurrent modifications. This enhancement enhances the reliability and stability of the Simplenote plugin. --- _config.py | 4 +- commands.py | 36 ++++++--- lib/core.py | 38 ++++++--- main.py | 8 +- utils/lock/thread.py | 181 +++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 240 insertions(+), 27 deletions(-) create mode 100644 utils/lock/thread.py diff --git a/_config.py b/_config.py index c589586..a64ea51 100644 --- a/_config.py +++ b/_config.py @@ -133,8 +133,8 @@ def __init_subclass__(cls, **kwargs): SIMPLENOTE_NOTES_DIR = os.path.join(SIMPLENOTE_CACHE_DIR, "notes") os.makedirs(SIMPLENOTE_NOTES_DIR, exist_ok=True) - # SIMPLENOTE_STARTED: bool = False - # SIMPLENOTE_RELOAD_CALLS: int = -1 + SIMPLENOTE_STARTED_KEY: str = "simplenote_started" + SIMPLENOTE_SYNC_TIMES_KEY: str = "simplenote_sync_times" class Development(_BaseConfig): diff --git a/commands.py b/commands.py index f184ad7..ef2087e 100644 --- a/commands.py +++ b/commands.py @@ -7,7 +7,7 @@ import sublime_plugin from ._config import CONFIG -from .lib.core import start +from .lib.core import GlobalStorage, sync from .lib.gui import close_view, on_note_changed, open_view, show_message, show_quick_panel from .lib.models import Note from .lib.operations import NoteCreator, NoteDeleter, NotesIndicator, NoteUpdater, OperationManager @@ -25,7 +25,7 @@ logger = logging.getLogger() -SIMPLENOTE_STARTED = False +global_storage = GlobalStorage() class SimplenoteViewCommand(sublime_plugin.EventListener): @@ -123,11 +123,17 @@ def on_post_save(self, view: sublime.View): class SimplenoteListCommand(sublime_plugin.ApplicationCommand): def run(self): - global SIMPLENOTE_STARTED - if not SIMPLENOTE_STARTED: - if not start(): - return - show_quick_panel() + sync_times = global_storage.get(CONFIG.SIMPLENOTE_SYNC_TIMES_KEY) + if not isinstance(sync_times, int): + raise TypeError( + "Value of %s must be type %s, got %s" % (CONFIG.SIMPLENOTE_SYNC_TIMES_KEY, int, type(sync_times)) + ) + first_sync = sync_times == 0 + if Note.tree.count: + show_quick_panel() + first_sync = False + if not global_storage.get(CONFIG.SIMPLENOTE_STARTED_KEY): + sync(first_sync) class SimplenoteSyncCommand(sublime_plugin.ApplicationCommand): @@ -141,13 +147,19 @@ def callback(self, updated_notes: List[Note], first_sync: bool = False): self.merge_note(updated_notes) if first_sync: show_quick_panel(first_sync) + global_storage.optimistic_update(CONFIG.SIMPLENOTE_STARTED_KEY, False) - def run(self, first_sync: bool = False): - settings = sublime.load_settings(CONFIG.SIMPLENOTE_SETTINGS_FILE_PATH) - sync_note_number = settings.get("sync_note_number", 1000) - if not isinstance(sync_note_number, int): - show_message("`sync_note_number` must be an integer. Please check settings file.") + def run(self, first_sync: bool = False, sync_note_number: int = 1000): + if global_storage.get(CONFIG.SIMPLENOTE_STARTED_KEY): return + global_storage.optimistic_update(CONFIG.SIMPLENOTE_STARTED_KEY, True) + sync_times = global_storage.get(CONFIG.SIMPLENOTE_SYNC_TIMES_KEY) + if not isinstance(sync_times, int): + raise TypeError( + "Value of %s must be type %s, got %s" % (CONFIG.SIMPLENOTE_SYNC_TIMES_KEY, int, type(sync_times)) + ) + sync_times += 1 + sync_times = global_storage.optimistic_update(CONFIG.SIMPLENOTE_SYNC_TIMES_KEY, sync_times) note_indicator = NotesIndicator(sync_note_number=sync_note_number) note_indicator.set_callback(self.callback, {"first_sync": first_sync}) OperationManager().add_operation(note_indicator) diff --git a/lib/core.py b/lib/core.py index 37c4aee..3f9d184 100644 --- a/lib/core.py +++ b/lib/core.py @@ -3,24 +3,42 @@ import sublime from .._config import CONFIG +from ..utils.lock.thread import OptimisticLockingDict +from ..utils.patterns.singleton.base import Singleton from .gui import edit_settings, remove_status, show_message from .operations import OperationManager logger = logging.getLogger() -SIMPLENOTE_STARTED = False + + +class GlobalStorage(Singleton, OptimisticLockingDict): + __mapper_key_type = {CONFIG.SIMPLENOTE_SYNC_TIMES_KEY: int, CONFIG.SIMPLENOTE_STARTED_KEY: bool} + + def optimistic_update(self, key, new_value): + _type = self.__mapper_key_type.get(key) + if not _type is None: + if not isinstance(new_value, _type): + raise TypeError("Value of %s must be type %s, got %s" % (key, _type, type(new_value))) + super().optimistic_update(key, new_value) + + manager = OperationManager() def sync(first_sync: bool = False): + + settings = sublime.load_settings(CONFIG.SIMPLENOTE_SETTINGS_FILE_PATH) + sync_note_number = settings.get("sync_note_number", 1000) + if not isinstance(sync_note_number, int): + show_message("`sync_note_number` must be an integer. Please check settings file.") + return if not manager.running: - sublime.run_command("simplenote_sync", {"first_sync": first_sync}) + sublime.run_command("simplenote_sync", {"first_sync": first_sync, "sync_note_number": sync_note_number}) else: logger.debug("Sync omitted") - settings = sublime.load_settings(CONFIG.SIMPLENOTE_SETTINGS_FILE_PATH) sync_every = settings.get("sync_every", 0) - logger.debug(("Simplenote sync_every", sync_every)) if not isinstance(sync_every, int): show_message("`sync_every` must be an integer. Please check settings file.") return @@ -30,17 +48,13 @@ def sync(first_sync: bool = False): def start(): - global SIMPLENOTE_STARTED settings = sublime.load_settings("Simplenote.sublime-settings") username = settings.get("username") password = settings.get("password") if username and password: sync(first_sync=True) - SIMPLENOTE_STARTED = True - else: - edit_settings() - show_message("Simplenote: Please configure username/password in settings file.") - sublime.set_timeout(remove_status, 2000) - SIMPLENOTE_STARTED = False - return SIMPLENOTE_STARTED + return + show_message("Simplenote: Please configure username/password in settings file.") + edit_settings() + sublime.set_timeout(remove_status, 2000) diff --git a/main.py b/main.py index c1854d1..35e40d0 100644 --- a/main.py +++ b/main.py @@ -3,7 +3,8 @@ import sublime -from .lib.core import start, sync +from ._config import CONFIG +from .lib.core import GlobalStorage, start, sync from .lib.models import Note @@ -12,6 +13,11 @@ SIMPLENOTE_RELOAD_CALLS = -1 +global_storage = GlobalStorage() +global_storage.optimistic_update(CONFIG.SIMPLENOTE_STARTED_KEY, False) +global_storage.optimistic_update(CONFIG.SIMPLENOTE_SYNC_TIMES_KEY, 0) + + def reload_if_needed(autostart: bool = True): # global SIMPLENOTE_RELOAD_CALLS diff --git a/utils/lock/thread.py b/utils/lock/thread.py new file mode 100644 index 0000000..d853264 --- /dev/null +++ b/utils/lock/thread.py @@ -0,0 +1,181 @@ +__version__ = "0.0.1" +__author__ = "redatman" +__date__ = "2024-08-03" +# TODO: ResultProcess unable to collect results yet + + +from importlib import import_module +import logging +from multiprocessing import Process +from threading import Thread +from typing import Callable + + +import_module("utils.logger.init") +logger = logging.getLogger() + + +class ResultExecutorMixin: + start: Callable + _target: Callable + _args: tuple + _kwargs: dict + _result = None + + def run(self): + try: + if self._target is not None: + self._result = self._target(*self._args, **self._kwargs) + finally: + del self._target, self._args, self._kwargs + + def join(self, *args): + super().join(*args) # type: ignore + # logger.warning(getattr(self, "_result", None)) + return getattr(self, "_result", None) + + def get_result(self): + self.start() + return self.join() + + +ResultProcess = type("ResultProcess", (ResultExecutorMixin, Process), {}) +ResultThread = type("ResultThread", (ResultExecutorMixin, Thread), {}) + + +class OptimisticLockingError(Exception): + def __init__(self, key: str) -> None: + super().__init__(f"Update failed due to concurrent modification: {key}") + + +class OptimisticLockingDict: + + def __init__(self, executor_cls=ResultThread): + if issubclass(executor_cls, Process): + from multiprocessing import Lock, Manager + + self.data = Manager().dict() + self.lock = Lock() + elif issubclass(executor_cls, Thread): + from threading import Lock + + self.data = {} + self.lock = Lock() + else: + raise ValueError( + f"Unsupported executor class: {executor_cls}, must be either multiprocessing.Process or threading.Thread" + ) + + def _get(self, key): + + # logger.debug(("_get", os.getpid(), threading.current_thread().name, threading.current_thread().ident)) + with self.lock: + if key in self.data: + value, version = self.data[key] + return value, version + else: + return None, None + + def get(self, key): + logger.info(self.data) + value, version = self._get(key) + return value + + def _set(self, key, new_value, expected_version): + # logger.warning((id(self.data), self.data)) + # logger.debug(("_set", os.getpid(), threading.current_thread().name, threading.current_thread().ident)) + with self.lock: + if key in self.data: + current_value, current_version = self.data[key] + if current_version == expected_version: + self.data[key] = (new_value, current_version + 1) + return True + else: + return False + else: + # If the key does not exist, initialize it + self.data[key] = (new_value, 1) + return True + + def set(self, key, new_value): + return self._set(key, new_value, 0) + + def optimistic_update(self, key, new_value): + # logger.warning((id(self), id(self.data))) + # logger.warning((id(self), self)) + # logger.debug(f">>: {key} = {new_value}") + value, version = self._get(key) + # time.sleep(0.1) + if value is not None: + success = self._set(key, new_value, version) + if success: + logger.debug(f"Update successful: {key} from {value} to {new_value}") + else: + logger.debug(f"Update failed due to concurrent modification: {key} to {new_value}") + raise OptimisticLockingError(key) + else: + # Initialize the key if it doesn't exist + self.set(key, new_value) + logger.debug(f"Initial set: {key} = {new_value}") + return new_value + + # def update(self, key, new_value): + # with self.lock: + # return self.optimistic_update(key, new_value) + + +def test_multiple_updates(executor_cls): + optimistic_dict = OptimisticLockingDict(executor_cls) + logger.warning((id(optimistic_dict), id(optimistic_dict.data))) + key = "name" + + # Initialize a key-value pair + optimistic_dict.optimistic_update(key, "value1") + + # tasks = [] + results = set() + + # Simulate concurrent updates + def concurrent_update(): + for i in range(6): + task = executor_cls(target=optimistic_dict.optimistic_update, args=("name", i)) + import time + + # time.sleep(0.01) + # tasks.append(task) + # task.start() + # result = task.join() + result = task.get_result() + logger.debug(result) + results.add(result) + + logger.info(results) + + concurrent_update() + last_result = optimistic_dict.get(key) + expected_result = 5 + assert last_result == expected_result, f"Expected last value is {expected_result}, but got %s" % last_result + expected_results = {0, 1, 2, 3, 4, 5} + assert results == expected_results, f"Expected results is {expected_results}, but got {results}" + + +def run_tests(): + tests = { + ("Test test_multiple_process_updates ", test_multiple_updates, (ResultProcess,)), + ("Test test_multiple_thread_updates ", test_multiple_updates, (ResultThread,)), + } + + for test_name, test, args in tests: + try: + prefix = f"Running [{test_name}]" + test(*args) + logger.info(f"{prefix} Succeeded") + except AssertionError as e: + logger.error(f"{prefix} Failed => {e}") + except Exception as e: + logger.critical(f"{prefix} Exception => {e}") + + +if __name__ == "__main__": + + run_tests() From c90aaaa672708f852814983dc5f1d33166b53d2f Mon Sep 17 00:00:00 2001 From: redatman Date: Sat, 3 Aug 2024 06:32:13 +0800 Subject: [PATCH 5/8] Fix: Exclude .env files from Sublime package The .env file should not be included in the Sublime package. This change ensures that sensitive information is not accidentally distributed to users. --- dev_deploy.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev_deploy.sh b/dev_deploy.sh index 47ddef6..68b4c76 100644 --- a/dev_deploy.sh +++ b/dev_deploy.sh @@ -1,4 +1,4 @@ # local development deploy -zip -r Simplenote.sublime-package . -x ".git/*" ".github/*" ".gitignore" ".idea/*" ".vscode/*" ".pytest_cache/*" "pyproject.toml" "package-lock.json" "package.json" "node_modules/*" ".env.*" ".DS_Store" "assets/*" "*__pycache__/*" "tmp/*" "tests/*" "logs/*" "sublime_api.py" "dev_deploy.sh" "package-metadata.json" +zip -r Simplenote.sublime-package . -x ".env*" ".git/*" ".github/*" ".gitignore" ".idea/*" ".vscode/*" ".pytest_cache/*" "pyproject.toml" "package-lock.json" "package.json" "node_modules/*" ".env.*" ".DS_Store" "assets/*" "*__pycache__/*" "tmp/*" "tests/*" "logs/*" "sublime_api.py" "dev_deploy.sh" "package-metadata.json" mv Simplenote.sublime-package $HOME/Library/Application\ Support/Sublime\ Text/Installed\ Packages/Simplenote.sublime-package From 7b52a49e230f8f5a96305a2e0e6ec4b09e4ca347 Mon Sep 17 00:00:00 2001 From: redatman Date: Sat, 3 Aug 2024 06:32:47 +0800 Subject: [PATCH 6/8] misc: Disable default log handlers Disables default log handlers to allow for more granular control over logging output. This change facilitates tailored logging configurations and reduces potential redundancy. --- utils/logger/init.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/utils/logger/init.py b/utils/logger/init.py index d91881b..be59ad1 100644 --- a/utils/logger/init.py +++ b/utils/logger/init.py @@ -188,10 +188,10 @@ "": { "handlers": [ # "default", - "info", - "warning", - "error", - "critical", + # "info", + # "warning", + # "error", + # "critical", # Keep console at the end. for colored output only at stdout. "console", ], From 55b5857a5f70ba577e615df8d2582adf26bc79a7 Mon Sep 17 00:00:00 2001 From: redatman Date: Sat, 3 Aug 2024 09:42:51 +0800 Subject: [PATCH 7/8] Fix: Simplify first sync logic The `sync_once` function now handles the first sync more effectively, eliminating the need for `first_sync` flags and streamlining the code. This simplifies the logic for both `sync` and `SimplenoteSyncCommand` and reduces the overall complexity. --- commands.py | 38 +++++++++++++++++++------------------- lib/core.py | 39 ++++++++++++++++++++++----------------- main.py | 32 +++++++++++++++----------------- 3 files changed, 56 insertions(+), 53 deletions(-) diff --git a/commands.py b/commands.py index ef2087e..9aef21c 100644 --- a/commands.py +++ b/commands.py @@ -7,7 +7,7 @@ import sublime_plugin from ._config import CONFIG -from .lib.core import GlobalStorage, sync +from .lib.core import GlobalStorage, sync_once from .lib.gui import close_view, on_note_changed, open_view, show_message, show_quick_panel from .lib.models import Note from .lib.operations import NoteCreator, NoteDeleter, NotesIndicator, NoteUpdater, OperationManager @@ -123,17 +123,10 @@ def on_post_save(self, view: sublime.View): class SimplenoteListCommand(sublime_plugin.ApplicationCommand): def run(self): - sync_times = global_storage.get(CONFIG.SIMPLENOTE_SYNC_TIMES_KEY) - if not isinstance(sync_times, int): - raise TypeError( - "Value of %s must be type %s, got %s" % (CONFIG.SIMPLENOTE_SYNC_TIMES_KEY, int, type(sync_times)) - ) - first_sync = sync_times == 0 if Note.tree.count: show_quick_panel() - first_sync = False if not global_storage.get(CONFIG.SIMPLENOTE_STARTED_KEY): - sync(first_sync) + sync_once() class SimplenoteSyncCommand(sublime_plugin.ApplicationCommand): @@ -143,25 +136,32 @@ def merge_note(self, updated_notes: List[Note]): if note.need_flush: on_note_changed(note) - def callback(self, updated_notes: List[Note], first_sync: bool = False): + def callback(self, updated_notes: List[Note]): self.merge_note(updated_notes) + + sync_times = global_storage.get(CONFIG.SIMPLENOTE_SYNC_TIMES_KEY) + if not isinstance(sync_times, int): + raise TypeError( + "Value of %s must be type %s, got %s" % (CONFIG.SIMPLENOTE_SYNC_TIMES_KEY, int, type(sync_times)) + ) + first_sync = sync_times == 0 if first_sync: show_quick_panel(first_sync) + global_storage.optimistic_update(CONFIG.SIMPLENOTE_SYNC_TIMES_KEY, sync_times + 1) global_storage.optimistic_update(CONFIG.SIMPLENOTE_STARTED_KEY, False) - def run(self, first_sync: bool = False, sync_note_number: int = 1000): + def run(self): if global_storage.get(CONFIG.SIMPLENOTE_STARTED_KEY): return global_storage.optimistic_update(CONFIG.SIMPLENOTE_STARTED_KEY, True) - sync_times = global_storage.get(CONFIG.SIMPLENOTE_SYNC_TIMES_KEY) - if not isinstance(sync_times, int): - raise TypeError( - "Value of %s must be type %s, got %s" % (CONFIG.SIMPLENOTE_SYNC_TIMES_KEY, int, type(sync_times)) - ) - sync_times += 1 - sync_times = global_storage.optimistic_update(CONFIG.SIMPLENOTE_SYNC_TIMES_KEY, sync_times) + + settings = sublime.load_settings(CONFIG.SIMPLENOTE_SETTINGS_FILE_PATH) + sync_note_number = settings.get("sync_note_number", 1000) + if not isinstance(sync_note_number, int): + show_message("`sync_note_number` must be an integer. Please check settings file.") + return note_indicator = NotesIndicator(sync_note_number=sync_note_number) - note_indicator.set_callback(self.callback, {"first_sync": first_sync}) + note_indicator.set_callback(self.callback) OperationManager().add_operation(note_indicator) diff --git a/lib/core.py b/lib/core.py index 3f9d184..081382d 100644 --- a/lib/core.py +++ b/lib/core.py @@ -20,40 +20,45 @@ def optimistic_update(self, key, new_value): if not _type is None: if not isinstance(new_value, _type): raise TypeError("Value of %s must be type %s, got %s" % (key, _type, type(new_value))) - super().optimistic_update(key, new_value) + if key == CONFIG.SIMPLENOTE_SYNC_TIMES_KEY: + import time -manager = OperationManager() + logger.warning((time.time(), key, new_value)) + return super().optimistic_update(key, new_value) -def sync(first_sync: bool = False): +manager = OperationManager() +global_storage = GlobalStorage() - settings = sublime.load_settings(CONFIG.SIMPLENOTE_SETTINGS_FILE_PATH) - sync_note_number = settings.get("sync_note_number", 1000) - if not isinstance(sync_note_number, int): - show_message("`sync_note_number` must be an integer. Please check settings file.") - return + +def sync_once(): if not manager.running: - sublime.run_command("simplenote_sync", {"first_sync": first_sync, "sync_note_number": sync_note_number}) + sublime.run_command("simplenote_sync") else: logger.debug("Sync omitted") - sync_every = settings.get("sync_every", 0) - if not isinstance(sync_every, int): - show_message("`sync_every` must be an integer. Please check settings file.") - return - if sync_every > 0: - sublime.set_timeout(sync, sync_every * 1000) +def sync(sync_every: int = 30): + sync_once() + sublime.set_timeout(sync, sync_every * 1000) def start(): - settings = sublime.load_settings("Simplenote.sublime-settings") + settings = sublime.load_settings(CONFIG.SIMPLENOTE_SETTINGS_FILE_PATH) username = settings.get("username") password = settings.get("password") if username and password: - sync(first_sync=True) + if global_storage.get(CONFIG.SIMPLENOTE_SYNC_TIMES_KEY) != 0: + return + sync_every = settings.get("sync_every", 0) + if not isinstance(sync_every, int): + show_message("`sync_every` must be an integer. Please check settings file.") + return + if sync_every <= 0: + return + sync(sync_every) return show_message("Simplenote: Please configure username/password in settings file.") edit_settings() diff --git a/main.py b/main.py index 35e40d0..7dd06ee 100644 --- a/main.py +++ b/main.py @@ -1,53 +1,51 @@ -import functools import logging import sublime from ._config import CONFIG -from .lib.core import GlobalStorage, start, sync +from .lib.core import GlobalStorage, start +from .lib.gui import show_message from .lib.models import Note logger = logging.getLogger() -SIMPLENOTE_RELOAD_CALLS = -1 - global_storage = GlobalStorage() global_storage.optimistic_update(CONFIG.SIMPLENOTE_STARTED_KEY, False) global_storage.optimistic_update(CONFIG.SIMPLENOTE_SYNC_TIMES_KEY, 0) -def reload_if_needed(autostart: bool = True): +def reload_if_needed(): # global SIMPLENOTE_RELOAD_CALLS # # Sublime calls this twice for some reason :( # SIMPLENOTE_RELOAD_CALLS += 1 + # logger.warning((SIMPLENOTE_RELOAD_CALLS, SIMPLENOTE_RELOAD_CALLS % 2)) # if SIMPLENOTE_RELOAD_CALLS % 2 != 0: # logger.debug("Simplenote Reload call %s" % SIMPLENOTE_RELOAD_CALLS) # return - logger.debug(("Simplenote Reloading", autostart)) - start() + settings = sublime.load_settings(CONFIG.SIMPLENOTE_SETTINGS_FILE_PATH) + autostart = settings.get("autostart", True) + if not isinstance(autostart, bool): + show_message("`autostart` must be a boolean. Please check settings file.") + return if autostart: - sublime.set_timeout_async(sync, 2000) - logger.debug("Auto Starting") + start() def plugin_loaded(): # load_notes() logger.debug(("Loaded notes number: ", Note.tree.count)) - settings = sublime.load_settings("Simplenote.sublime-settings") + settings = sublime.load_settings(CONFIG.SIMPLENOTE_SETTINGS_FILE_PATH) # logger.debug(("SETTINGS.__dict__: ", SETTINGS.__dict__)) # logger.debug(("SETTINGS.username: ", SETTINGS.get("username"))) - autostart = settings.get("autostart", True) - if not isinstance(autostart, bool): - autostart = True - callback = functools.partial(reload_if_needed, autostart) + settings.clear_on_change("username") settings.clear_on_change("password") - settings.add_on_change("username", callback) - settings.add_on_change("password", callback) + settings.add_on_change("username", reload_if_needed) + settings.add_on_change("password", reload_if_needed) - reload_if_needed(autostart=autostart) + reload_if_needed() From f5e91e6d50823988b7b732accdfe3d0bac7f05dc Mon Sep 17 00:00:00 2001 From: redatman Date: Sat, 3 Aug 2024 09:46:17 +0800 Subject: [PATCH 8/8] Refactor: Rename `sync_every` to `sync_interval` Renamed the `sync_every` setting to `sync_interval` for clarity and consistency. This improves the readability and understanding of the plugin's settings. --- README.md | 2 +- Simplenote.sublime-settings | 4 ++-- _config.py | 2 +- lib/core.py | 14 +++++++------- lib/gui.py | 1 - 5 files changed, 11 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index ad89bc2..554f079 100644 --- a/README.md +++ b/README.md @@ -113,7 +113,7 @@ After setting them it will download the notes. Hit the shortcut again after the download is done (check the message bar) and it will **show a list of the notes**: ![Alt Notes](assets/images/note_list.png "Note List") -It will download notes every time sublime text is launched and every now and then if the _sync_every_ configuration is enabled (has a positive value), so take a look at the bar to check the status. +It will download notes every time sublime text is launched and every now and then if the _sync_interval_ configuration is enabled (has a positive value), so take a look at the bar to check the status. If a note gets updated from somewhere else ![Alt External Update](http://i.imgur.com/p9pAY6z.png "External Update") diff --git a/Simplenote.sublime-settings b/Simplenote.sublime-settings index 5a49b37..05d6676 100644 --- a/Simplenote.sublime-settings +++ b/Simplenote.sublime-settings @@ -9,8 +9,8 @@ // -------------------------------- // Sync when sublime text starts: ,"autostart": true - // Sync automatically (in seconds) - ,"sync_every": 30 + // Sync automatically interval (in seconds) + ,"sync_interval": 30 // Number of notes synchronized each time ,"sync_note_number": 1000 // Conflict resolution (If a file was edited on another client and also here, on sync..) diff --git a/_config.py b/_config.py index a64ea51..518f400 100644 --- a/_config.py +++ b/_config.py @@ -40,7 +40,7 @@ // Sync when sublime text starts: ,"autostart": true // Sync automatically (in seconds) - ,"sync_every": 30 + ,"sync_interval": 30 // Number of notes synchronized each time ,"sync_note_number": 1000 // Conflict resolution (If a file was edited on another client and also here, on sync..) diff --git a/lib/core.py b/lib/core.py index 081382d..0188e29 100644 --- a/lib/core.py +++ b/lib/core.py @@ -39,9 +39,9 @@ def sync_once(): logger.debug("Sync omitted") -def sync(sync_every: int = 30): +def sync(sync_interval: int = 30): sync_once() - sublime.set_timeout(sync, sync_every * 1000) + sublime.set_timeout(sync, sync_interval * 1000) def start(): @@ -52,13 +52,13 @@ def start(): if username and password: if global_storage.get(CONFIG.SIMPLENOTE_SYNC_TIMES_KEY) != 0: return - sync_every = settings.get("sync_every", 0) - if not isinstance(sync_every, int): - show_message("`sync_every` must be an integer. Please check settings file.") + sync_interval = settings.get("sync_interval", 30) + if not isinstance(sync_interval, int): + show_message("`sync_interval` must be an integer. Please check settings file.") return - if sync_every <= 0: + if sync_interval <= 0: return - sync(sync_every) + sync(sync_interval) return show_message("Simplenote: Please configure username/password in settings file.") edit_settings() diff --git a/lib/gui.py b/lib/gui.py index e512010..b26bd9d 100644 --- a/lib/gui.py +++ b/lib/gui.py @@ -138,7 +138,6 @@ def on_select(list__modificationDate: List[float], selected_index: int): def show_quick_panel(first_sync: bool = False): - logger.warning("show_quick_panel") if Note.tree.count <= 0: show_message( "No notes found. Please wait for the synchronization to complete, or press [super+shift+s, super+shift+c] to create a note."