Пример #1
0
def test_Theme_get_syntax_color():
    config = Config(None, { "theme": {
        "text_color": ColorString("000000"),
        "syntax": {
            "default": {
                "builtin": ColorString("bbbbbb"),
                "comment": ColorString("cccccc"),
                "keyword": ColorString("eeeeee"),
                "string": ColorString("accafe"),
            },
            "B": {
                "builtin": ColorString("BBBBBB"),
                "keyword": ColorString("EEEEEE"),
            },
        },
    }})
    config.data = {"theme": {"syntax": {
        "default": {
            "comment": "112233",
            "keyword": "445566",
        },
        "C": {
            "builtin": "665544",
            "keyword": "332211",
        },
    }}}
    theme = mod.Theme(config)
    get_color = lambda v: v
    def test(name, color):
        with replattr(mod, "get_color", get_color, sigcheck=False):
            eq_(theme.get_syntax_color(name), color)
    yield test, "A builtin", "bbbbbb"
    yield test, "A comment", "112233"
    yield test, "A keyword", "445566"
    yield test, "A string", "accafe"
    yield test, "B builtin", "BBBBBB"
    yield test, "B comment", "112233"
    yield test, "B keyword", "EEEEEE"
    yield test, "B string", "accafe"
    yield test, "C builtin", "665544"
    yield test, "C comment", "112233"
    yield test, "C keyword", "332211"
    yield test, "C string", "accafe"
Пример #2
0
 def __init__(self, profile=None):
     if profile is None:
         profile = self.default_profile()
     self.profile_path = os.path.expanduser(profile)
     assert os.path.isabs(self.profile_path), \
         'profile path cannot be relative (%s)' % self.profile_path
     self._setup_profile = set()
     self.editors = []
     self.path_opener = None
     self.config = Config(self.profile_path)
     self.context = ContextMap()
     self.syntax_factory = None
     state_dir = os.path.join(self.profile_path, const.STATE_DIR)
     command_history = CommandHistory(state_dir)
     self.text_commander = TextCommandController(command_history)
     register_value_transformers()
Пример #3
0
 def __init__(self, profile=None):
     self.launch_fault = False
     self.launching = True
     if profile is None:
         profile = self.default_profile()
     self.profile_path = os.path.expanduser(profile)
     assert os.path.isabs(self.profile_path), \
         'profile path cannot be relative (%s)' % self.profile_path
     self.documents = DocumentController(self)
     self.terminating = False
     self.errlog = ErrorLog(self)
     self.errlog_handler = LogViewHandler(self)
     self.errlog_handler.setLevel(logging.INFO)
     self.errlog_handler.setFormatter(
         logging.Formatter("%(levelname).7s %(name)s - %(message)s"))
     with self.logger():
         self.panels = []
         self._setup_profile = set()
         self.windows = []
         self.path_opener = None
         self.config_callbacks = WeakValueDictionary()
         self.config = Config(
             os.path.join(self.profile_path, const.CONFIG_FILENAME))
         logging_config = self.config["logging_config"]
         if logging_config:
             try:
                 logging.config.dictConfig(logging_config)
                 log.debug("logging config: %r", logging_config)
             except Exception:
                 log.exception("bad logging config: %r", logging_config)
         self.context = ContextMap()
         self.syntax_factory = None
         self.theme = Theme(self.config)
         state_dir = os.path.join(self.profile_path, const.STATE_DIR)
         command_history = CommandHistory(state_dir)
         self.text_commander = CommandManager(command_history, app=self)
Пример #4
0
class Application(object):

    def __init__(self, profile=None):
        self.launch_fault = False
        self.launching = True
        if profile is None:
            profile = self.default_profile()
        self.profile_path = os.path.expanduser(profile)
        assert os.path.isabs(self.profile_path), \
            'profile path cannot be relative (%s)' % self.profile_path
        self.documents = DocumentController(self)
        self.terminating = False
        self.errlog = ErrorLog(self)
        self.errlog_handler = LogViewHandler(self)
        self.errlog_handler.setLevel(logging.INFO)
        self.errlog_handler.setFormatter(
            logging.Formatter("%(levelname).7s %(name)s - %(message)s"))
        with self.logger():
            self.panels = []
            self._setup_profile = set()
            self.windows = []
            self.path_opener = None
            self.config_callbacks = WeakValueDictionary()
            self.config = Config(
                os.path.join(self.profile_path, const.CONFIG_FILENAME))
            logging_config = self.config["logging_config"]
            if logging_config:
                try:
                    logging.config.dictConfig(logging_config)
                    log.debug("logging config: %r", logging_config)
                except Exception:
                    log.exception("bad logging config: %r", logging_config)
            self.context = ContextMap()
            self.syntax_factory = None
            self.theme = Theme(self.config)
            state_dir = os.path.join(self.profile_path, const.STATE_DIR)
            command_history = CommandHistory(state_dir)
            self.text_commander = CommandManager(command_history, app=self)

    @classmethod
    def name(cls):
        return fn.NSBundle.mainBundle().objectForInfoDictionaryKey_("CFBundleName")

    @classmethod
    def resource_path(cls):
        return fn.NSBundle.mainBundle().resourcePath()

    @classmethod
    def default_profile(cls):
        return '~/.' + cls.name().lower()

    @contextmanager
    def logger(self):
        root = logging.getLogger()
        root.addHandler(self.errlog_handler)
        try:
            yield self.errlog
        finally:
            root.removeHandler(self.errlog_handler)

    def init_syntax_definitions(self):
        from editxt.syntax import SyntaxFactory
        self.syntax_factory = sf = SyntaxFactory()
        paths = [(self.resource_path(), False), (self.profile_path, True)]
        for path, log_info in paths:
            path = os.path.join(path, const.SYNTAX_DEFS_DIR)
            sf.load_definitions(path, log_info)
        sf.index_definitions()

    @property
    def syntaxdefs(self):
        return self.syntax_factory.definitions

    @property
    def default_font(self, cache=[]):
        """Get the default application font

        Uses the value specified in the config file (all fields are optinal):

        ```
        font:
            face: Inconsolata
            size: 13
            smooth: true
        ```

        Falling back to the default system fixed width font for unspecified
        or unknown values.

        :returns: A `editxt.datatypes.Font` instance.
        """
        cfg = self.config["font"]
        key = (cfg["face"], cfg["size"], cfg["smooth"])
        if not (cache and cache[0] == key):
            cache[:] = [key, get_font(*key)]
        return cache[1]

    def reload_config(self):
        old_theme = self.config.get("theme")
        self.config.reload()
        self.theme.reset()
        class event:
            theme_changed = self.config.get("theme") != old_theme
        for callback in self.config_callbacks:
            # without call_later this could cause
            # [NSMachPort sendBeforeDate:] destination port invalid
            # due to forced syntax highlighting taking a long time
            call_later(0, callback, event=event)

    def on_reload_config(self, callback, owner):
        self.config_callbacks[callback] = owner

    def application_will_finish_launching(self, app, delegate):
        self.init_syntax_definitions()
        self.text_commander.load_commands(delegate.textMenu)
        self.text_commander.load_shortcuts(delegate.shortcutsMenu)
        states = list(self.iter_saved_window_states())
        errors = []
        if states:
            for state in reversed(states):
                if isinstance(state, StateLoadFailure):
                    errors.append(state)
                else:
                    self.create_window(state)
        else:
            self.create_window()
        self.launching = False
        if self.launch_fault or errors:
            self.open_error_log(set_current=False)

    def create_window(self, state=None):
        from editxt.window import Window
        if state is None:
            state_dir = os.path.join(self.profile_path, const.STATE_DIR, const.CLOSED_DIR)
            for x, path in enumerate_state_paths(state_dir, reverse=True):
                try:
                    with open(path, encoding="utf-8") as f:
                        state = load_yaml(f)
                except Exception:
                    log.warn("cannot load editor state: %s", path)
                    continue
                try:
                    os.remove(path)
                except Exception:
                    log.exception('cannot remove %s', path)
                break
        window = Window(self, state)
        self.windows.append(window)
        window.show(self)
        return window

    def open_path_dialog(self):
        if self.path_opener is None:
            opc = OpenPathController(self)
            opc.showWindow_(self)
            self.path_opener = opc
        else:
           self.path_opener.window().makeKeyAndOrderFront_(self)
        self.path_opener.populateWithClipboard()

    def new_document(self):
        self.open_documents_with_paths([None])

    def new_project(self):
        window = self.current_window()
        if window is not None:
            return window.new_project()

    def document_with_path(self, path):
        """Get a text document with the given path

        Documents returned by this method have been added to the document
        controllers list of documents.
        """
        from editxt.document import TextDocument
        docs = self.documents
        if path is None:
            # untitled document will get associated with DocumentController on save
            doc = TextDocument(self)
        else:
            doc = docs.get_document(path)
        return doc

    def open_documents_with_paths(self, paths):
        window = self.current_window()
        if window is None:
            window = self.create_window()
        return window.open_paths(paths)

    def open_config_file(self):
        items = self.open_documents_with_paths([self.config.path])
        assert len(items) == 1, items
        editor = items[0]
        assert editor.file_path == self.config.path, \
            (editor.file_path, self.config.path)
        if not (os.path.exists(editor.file_path) or editor.document.text):
            editor.document.text = self.config.default_config

    def open_error_log(self, set_current=True):
        """Open the error log document

        This method should only be called while this application's
        ``logger`` context manager is active.
        """
        doc = self.errlog.document
        try:
            editor = next(self.iter_editors_of_document(doc))
        except StopIteration:
            window = self.current_window()
            if window is None:
                window = self.create_window()
            window.insert_items([doc], focus=set_current)
        else:
            if set_current:
                self.set_current_editor(editor)

    def get_internal_document(self, name):
        if name == "errlog":
            return self.errlog.document
        raise Error("unknown internal document: {}".format(name))

    def iter_dirty_editors(self):
        seen = set()
        for window in self.iter_windows():
            for proj in window.projects:
                dirty = False
                for editor in proj.dirty_editors():
                    if editor.document.id not in seen:
                        seen.add(editor.document.id)
                        yield editor
                        dirty = True
                if dirty:
                    yield proj

    def set_current_editor(self, editor):
        window = self.find_window_with_editor(editor)
        window.current_editor = editor

    def close_current_document(self):
        window = self.current_window()
        if window is not None:
            editor = window.current_editor
            if editor is not None:
                window.close_item(editor)

    def document_closed(self, document):
        """Remove document from the list of open documents"""
        self.documents.discard(document)

    def iter_editors_of_document(self, doc):
        for window in self.iter_windows():
            for editor in window.iter_editors_of_document(doc):
                yield editor

    def iter_windows_with_editor_of_document(self, document):
        for window in self.iter_windows():
            try:
                next(window.iter_editors_of_document(document))
            except StopIteration:
                pass
            else:
                yield window

    def find_window_with_editor(self, editor):
        for window in self.iter_windows():
            for proj in window.projects:
                for ed in proj.editors:
                    if ed is editor:
                        return window
        return None

    def find_windows_with_project(self, project):
        return [ed for ed in self.windows if project in ed.projects]

    def find_project_with_path(self, path):
        for ed in self.windows:
            proj = ed.find_project_with_path(path)
            if proj is not None:
                return proj
        return None

    def find_item_with_id(self, ident):
        # HACK slow implementation, violates encapsulation
        for window in self.windows:
            for proj in window.projects:
                if proj.id == ident:
                    return proj
                for doc in proj.editors:
                    if doc.id == ident:
                        return doc
        return None

    def iter_windows(self, app=None):
        """Iterate over windows in on-screen z-order starting with the
        front-most window"""
        from editxt.window import WindowController
        if app is None:
            app = ak.NSApp()
        z_ordered_eds = set()
        for win in app.orderedWindows():
            wc = win.windowController()
            if isinstance(wc, WindowController) and wc.window_ in self.windows:
                z_ordered_eds.add(wc.window_)
                yield wc.window_
        for ed in self.windows:
            if ed not in z_ordered_eds:
                yield ed

    def add_window(self, window):
        self.windows.append(window)

    def current_window(self):
        try:
            return next(self.iter_windows())
        except StopIteration:
            return None

    def discard_window(self, window):
        try:
            self.windows.remove(window)
        except ValueError:
            pass
        if self.setup_profile(windows=True) and not self.terminating:
            state_dir = os.path.join(self.profile_path, const.STATE_DIR, const.CLOSED_DIR)
            ident = next(enumerate_state_paths(state_dir, reverse=True), [-1])[0] + 1
            self.save_window_state(window, state_dir, ident)
        window.close()

    def setup_profile(self, windows=False):
        """Ensure that profile dir exists

        This will create the profile directory if it does not exist.

        :returns: True on success, otherwise False.
        """
        def setup(path, name):
            if name not in self._setup_profile and not os.path.isdir(path):
                try:
                    os.mkdir(path)
                except OSError:
                    log.error('cannot create %s', path, exc_info=True)
                    return False
                self._setup_profile.add(name)
            return True
        state_path = os.path.join(self.profile_path, const.STATE_DIR)
        closed_path = os.path.join(state_path, const.CLOSED_DIR)
        return setup(self.profile_path, ".") and (
            not windows or
            (setup(state_path, "editor") and setup(closed_path, "closed"))
        )

    def _legacy_window_states(self):
        # TODO remove once all users have upraded to new state persistence
        def pythonify(value):
            if isinstance(value, (str, int, float, bool)):
                return value
            if isinstance(value, (dict, fn.NSDictionary)):
                return {k: pythonify(v) for k, v in value.items()}
            if isinstance(value, (list, fn.NSArray)):
                return [pythonify(v) for v in value]
            raise ValueError('unknown value type: {} {}'
                .format(type(value), repr(value)))
        defaults = fn.NSUserDefaults.standardUserDefaults()
        serials = defaults.arrayForKey_(const.WINDOW_CONTROLLERS_DEFAULTS_KEY) or []
        settings = defaults.arrayForKey_(const.WINDOW_SETTINGS_DEFAULTS_KEY) or []
        serials = list(reversed(serials))
        for serial, setting in zip(serials, chain(settings, repeat(None))):
            try:
                state = dict(serial)
                if setting is not None:
                    state['window_settings'] = setting
                yield pythonify(state)
            except Exception:
                log.warn('cannot load legacy state: %r', serial, exc_info=True)

    def iter_saved_window_states(self):
        """Yield saved window states"""
        state_dir = os.path.join(self.profile_path, const.STATE_DIR)
        if not os.path.exists(state_dir):
            if self.profile_path == os.path.expanduser(self.default_profile()):
                # TODO remove once all users have upgraded
                for state in self._legacy_window_states():
                    yield state
            return
        for x, path in enumerate_state_paths(state_dir):
            try:
                with open(path, encoding="utf-8") as f:
                    yield load_yaml(f)
            except Exception:
                log.error('cannot load %s', path, exc_info=True)
                yield StateLoadFailure(path)

    def save_window_state(self, window, state_dir, ident):
        """Save a single window's state

        :param window: The window with state to be saved.
        :param state_dir: The directory in which to write the state file.
        :param ident: The identifier to use when saving window state.
        :returns: The name of the state file.
        """
        state_name = const.EDITOR_STATE.format(ident)
        state_file = os.path.join(state_dir, state_name)
        state = window.state
        try:
            with atomicfile(state_file, encoding="utf-8") as fh:
                dump_yaml(state, fh)
        except Exception:
            log.error('cannot write %s\n%s\n', state_file, state, exc_info=True)
        return state_name

    def save_window_states(self):
        """Save all windows' states"""
        state_dir = os.path.join(self.profile_path, const.STATE_DIR)
        old = {os.path.basename(x[1]) for x in enumerate_state_paths(state_dir)}
        for i, window in enumerate(self.iter_windows()):
            if i == 0:
                self.setup_profile(windows=True)
            state_name = self.save_window_state(window, state_dir, i)
            old.discard(state_name)
        for name in old:
            state_file = os.path.join(state_dir, name)
            try:
                os.remove(state_file)
            except Exception:
                log.error('cannot remove %s', state_file, exc_info=True)

    def should_terminate(self, callback):
        """Check if application can terminate

        :param callback: A callback to be used as the return value to signal
        a delayed termination. If this object is returned it will be called with
        a single boolean argument at some point in the future when it is known
        if the application should terminate or not.
        :returns: ``True``, ``False`` or ``callback``. ``True`` means the
        application should terminate immediately. ``False`` means the
        application should not terminate, and ``callback`` means the application
        will perform an extended check to determine if it should terminate
        before calling ``callback`` with either ``True`` or ``False``.
        """
        if next(self.iter_dirty_editors(), None) is not None:
            self.async_interactive_close(self.iter_dirty_editors(), callback)
            return callback
        return True

    @staticmethod
    def async_interactive_close(dirty_editors, callback):
        """Visit each dirty document and prompt for close

        Calls ``callback(False)`` if any unsaved document cancels the
        close operation. Otherwise calls ``callback(True)``.
        """
        class RecursiveCall(Exception): pass

        def continue_closing():
            def _callback(ok_to_close):
                if not ok_to_close:
                    callback(False)
                    return
                if recursive_call:
                    raise RecursiveCall()
                continue_closing()
            recursive_call = True
            for editor in dirty_editors:
                try:
                    editor.should_close(_callback)
                except RecursiveCall:
                    continue
                except Exception:
                    log.exception("termination sequence failed")
                    callback(False)
                    return
                recursive_call = False
                return
            callback(True)

        dirty_editors = iter(dirty_editors)
        continue_closing()

    def will_terminate(self):
        self.terminating = True
        self.save_window_states()