Example #1
0
def test_SyntaxCache_syntaxdef():
    m = Mocker()
    syn = SyntaxCache()
    syn.cache.append("something")
    sd = m.mock(SyntaxDefinition)
    with m:
        assert syn.syntaxdef is not sd
        syn.syntaxdef = sd
        eq_(syn.cache, [])
        eq_(syn.syntaxdef, sd)
        syn.cache.append("something")
        eq_(syn.cache, ["something"])
        eq_(syn.syntaxdef, sd)
Example #2
0
def test_SyntaxCache_syntaxdef():
    m = Mocker()
    syn = SyntaxCache()
    syn.cache.append("something")
    sd = m.mock(SyntaxDefinition)
    with m:
        assert syn.syntaxdef is not sd
        syn.syntaxdef = sd
        eq_(syn.cache, [])
        eq_(syn.syntaxdef, sd)
        syn.cache.append("something")
        eq_(syn.cache, ["something"])
        eq_(syn.syntaxdef, sd)
Example #3
0
 def init(self):
     super(TextDocument, self).init()
     self.setUndoManager_(UndoManager.alloc().init())
     self.id = next(doc_id_gen)
     self.icon_cache = (None, None)
     self.document_attrs = {
         ak.NSDocumentTypeDocumentAttribute: ak.NSPlainTextDocumentType,
         ak.NSCharacterEncodingDocumentAttribute: fn.NSUTF8StringEncoding,
     }
     self.text_storage = ak.NSTextStorage.alloc(
     ).initWithString_attributes_("", {})
     self.syntaxer = SyntaxCache()
     self._filestat = None
     self.props = KVOProxy(self)
     self.indent_mode = app.config["indent.mode"]
     self.indent_size = app.config[
         "indent.size"]  # should come from syntax definition
     self.newline_mode = app.config["newline_mode"]
     self.highlight_selected_text = app.config[
         "highlight_selected_text.enabled"]
     self.reset_text_attributes(self.indent_size)
     #self.save_hooks = []
     return self
Example #4
0
 def init(self):
     super(TextDocument, self).init()
     self.setUndoManager_(UndoManager.alloc().init())
     self.id = doc_id_gen.next()
     self.icon_cache = (None, None)
     self.document_attrs = {
         NSDocumentTypeDocumentAttribute: NSPlainTextDocumentType,
         NSCharacterEncodingDocumentAttribute: NSUTF8StringEncoding,
     }
     self.text_storage = NSTextStorage.alloc().initWithString_attributes_(u"", {})
     self.syntaxer = SyntaxCache()
     self._filestat = None
     self.props = KVOProxy(self)
     self.indent_mode = const.INDENT_MODE_SPACE
     self.indent_size = 4  # should come from syntax definition
     self.newline_mode = const.NEWLINE_MODE_UNIX
     self.reset_text_attributes(self.indent_size)
     # self.save_hooks = []
     return self
Example #5
0
 def init(self):
     super(TextDocument, self).init()
     self.setUndoManager_(UndoManager.alloc().init())
     self.id = next(doc_id_gen)
     self.icon_cache = (None, None)
     self.document_attrs = {
         ak.NSDocumentTypeDocumentAttribute: ak.NSPlainTextDocumentType,
         ak.NSCharacterEncodingDocumentAttribute: fn.NSUTF8StringEncoding,
     }
     self.text_storage = ak.NSTextStorage.alloc().initWithString_attributes_("", {})
     self.syntaxer = SyntaxCache()
     self._filestat = None
     self.props = KVOProxy(self)
     self.indent_mode = app.config["indent.mode"]
     self.indent_size = app.config["indent.size"] # should come from syntax definition
     self.newline_mode = app.config["newline_mode"]
     self.highlight_selected_text = app.config["highlight_selected_text.enabled"]
     self.reset_text_attributes(self.indent_size)
     #self.save_hooks = []
     return self
Example #6
0
class TextDocument(ak.NSDocument):
    @classmethod
    def get_with_path(cls, path):
        """Get a document with the given path

        Documents returned by this method have been added to the document
        controllers list of documents.
        """
        url = fn.NSURL.fileURLWithPath_(path)
        dc = ak.NSDocumentController.sharedDocumentController()
        doc = dc.documentForURL_(url)
        if doc is None:
            if os.path.exists(path):
                doctype, err = dc.typeForContentsOfURL_error_(url, None)
                doc, err = dc.makeDocumentWithContentsOfURL_ofType_error_(
                    url, doctype, None)
                if err is not None:
                    raise Error(err.localizedFailureReason())
                if doc is None:
                    raise Error("could not open document: %s" % path)
                dc.addDocument_(doc)
            else:
                doc, err = dc.makeUntitledDocumentOfType_error_(
                    const.TEXT_DOCUMENT, None)
                doc.setFileURL_(url)
                doc.update_syntaxer()
        return doc

    def init(self):
        super(TextDocument, self).init()
        self.setUndoManager_(UndoManager.alloc().init())
        self.id = next(doc_id_gen)
        self.icon_cache = (None, None)
        self.document_attrs = {
            ak.NSDocumentTypeDocumentAttribute: ak.NSPlainTextDocumentType,
            ak.NSCharacterEncodingDocumentAttribute: fn.NSUTF8StringEncoding,
        }
        self.text_storage = ak.NSTextStorage.alloc(
        ).initWithString_attributes_("", {})
        self.syntaxer = SyntaxCache()
        self._filestat = None
        self.props = KVOProxy(self)
        self.indent_mode = app.config["indent.mode"]
        self.indent_size = app.config[
            "indent.size"]  # should come from syntax definition
        self.newline_mode = app.config["newline_mode"]
        self.highlight_selected_text = app.config[
            "highlight_selected_text.enabled"]
        self.reset_text_attributes(self.indent_size)
        #self.save_hooks = []
        return self

    def properties(self):
        return self.props

    def setProperties_(self, value):
        pass

    @property
    def text(self):
        return self.text_storage.mutableString()

    @text.setter
    def text(self, value):
        self.text_storage.mutableString().setString_(value)
        self.reset_text_attributes(self.indent_size)

    @property
    def newline_mode(self):
        return self._newline_mode

    @newline_mode.setter
    def newline_mode(self, value):
        self._newline_mode = value
        self.eol = const.EOLS[value]

    @property
    def character_encoding(self):
        return self.document_attrs.get(ak.NSCharacterEncodingDocumentAttribute)

    @character_encoding.setter
    def character_encoding(self, value):
        # TODO when value is None encoding should be removed from document_attrs
        if value is not None:
            self.document_attrs[
                ak.NSCharacterEncodingDocumentAttribute] = value
        else:
            self.document_attrs.pop(ak.NSCharacterEncodingDocumentAttribute,
                                    None)

    def reset_text_attributes(self, indent_size):
        font = ak.NSFont.fontWithName_size_("Monaco", 10.0)
        spcw = font.screenFontWithRenderingMode_(ak.NSFontDefaultRenderingMode) \
            .advancementForGlyph_(ord(" ")).width
        ps = ak.NSParagraphStyle.defaultParagraphStyle().mutableCopy()
        ps.setTabStops_([])
        ps.setDefaultTabInterval_(spcw * indent_size)
        ps = ps.copy()
        self._text_attributes = attrs = {
            ak.NSFontAttributeName: font,
            ak.NSParagraphStyleAttributeName: ps,
        }
        range = fn.NSMakeRange(0, self.text_storage.length())
        self.text_storage.addAttributes_range_(attrs, range)
        for view in app.iter_views_of_document(self):
            if view.text_view is not None:
                view.text_view.setTypingAttributes_(attrs)
                view.text_view.setDefaultParagraphStyle_(ps)

    def default_text_attributes(self):
        return self._text_attributes

    def makeWindowControllers(self):
        editor = app.current_editor()
        if editor is None:
            editor = app.create_editor()
        view = TextDocumentView.create_with_document(self)
        editor.add_document_view(view)
        self.addWindowController_(editor.wc)
        editor.current_view = view

    def readFromData_ofType_error_(self, data, doctype, error):
        success, err = self.read_data_into_textstorage(data, self.text_storage)
        if success:
            self.analyze_content()
        return (success, err)

    def read_data_into_textstorage(self, data, text_storage):
        options = {
            ak.NSDefaultAttributesDocumentOption:
            self.default_text_attributes()
        }
        options.update(self.document_attrs)
        while True:
            success, attrs, err = text_storage \
                .readFromData_options_documentAttributes_error_(
                    data, options, None, None)
            if success or ak.NSCharacterEncodingDocumentAttribute not in options:
                if success:
                    self.document_attrs = attrs
                break
            if err:
                log.error(err)
            options.pop(ak.NSCharacterEncodingDocumentAttribute, None)
        return success, err

    def dataOfType_error_(self, doctype, error):
        range = fn.NSMakeRange(0, self.text_storage.length())
        attrs = self.document_attrs
        data, err = self.text_storage \
            .dataFromRange_documentAttributes_error_(range, attrs, None)
        if err is None:
            try:
                self.update_syntaxer()
                app.save_editor_states()
            except Exception:
                log.error("unexpected error", exc_info=True)
#             if self.project is not None:
#                 self.project.save()
#                 self.updateSyntaxer()
#             if self.text_view is not None:
#                 # make the undo manager recognize edits after save
#                 self.text_view.breakUndoCoalescing()
        return (data, err)

    def setFileModificationDate_(self, date):
        super(TextDocument, self).setFileModificationDate_(date)
        self._filestat = None

    def analyze_content(self):
        text = self.text_storage.string()
        start, end, cend = text.getLineStart_end_contentsEnd_forRange_(
            None, None, None, (0, 0))
        if end != cend:
            eol = EOLREF.get(text[cend:end], const.NEWLINE_MODE_UNIX)
            self.newline_mode = eol
        mode, size = calculate_indent_mode_and_size(text)
        if size is not None:
            self.indent_size = size
        if mode is not None:
            self.indent_mode = mode

    def is_externally_modified(self):
        """check if this document has been modified by another program"""
        url = self.fileURL()
        if url is not None and os.path.exists(url.path()):
            ok, mdate, err = url.getResourceValue_forKey_error_(
                None, fn.NSURLContentModificationDateKey, None)
            if ok:
                return self.fileModificationDate() != mdate
        return None

    def check_for_external_changes(self, window):
        if not self.is_externally_modified():
            return
        if self.isDocumentEdited():
            if window is None:
                return  # ignore change (no gui for alert)
            stat = filestat(self.fileURL().path())
            if self._filestat == stat:
                return
            self._filestat = stat

            def callback(code):
                if code == ak.NSAlertFirstButtonReturn:
                    self.reload_document()

            alert = Alert.alloc().init()
            alert.setMessageText_("“%s” source document changed" %
                                  self.displayName())
            alert.setInformativeText_("Discard changes and reload?")
            alert.addButtonWithTitle_("Reload")
            alert.addButtonWithTitle_("Cancel")
            alert.beginSheetModalForWindow_withCallback_(window, callback)
        else:
            self.reload_document()

    @refactor(
        "improve undo after reload - use difflib to replace changed text only")
    def reload_document(self):
        """Reload document with the given URL

        This implementation allows the user to undo beyond the reload. The
        down-side is that it may use a lot of memory if the document is very
        large.
        """
        url = self.fileURL()
        if url is None or not os.path.exists(url.path()):
            return
        undo = self.undoManager()
        undo.should_remove = False
        textstore = self.text_storage
        self.text_storage = ak.NSTextStorage.alloc().init()
        try:
            ok, err = self.revertToContentsOfURL_ofType_error_(
                url, self.fileType(), None)
        finally:
            tempstore = self.text_storage
            self.text_storage = textstore
            undo.should_remove = True
        if not ok:
            log.error("could not reload document: %s", err)
            return  # TODO report err
        textview = None
        for view in app.iter_views_of_document(self):
            textview = view.text_view
            if textview is not None:
                break
        text = tempstore.string()
        range = fn.NSRange(0, textstore.length())
        if textview is None:
            textstore.replaceCharactersInRange_withString_(range, text)
            undo.removeAllActions()
        elif textview.shouldChangeTextInRange_replacementString_(range, text):
            #state = self.documentState
            textstore.replaceCharactersInRange_withString_(range, text)
            #self.documentState = state
            textview.didChangeText()
            textview.breakUndoCoalescing()
            # HACK use timed invocation to allow didChangeText notification
            # to update change count before _clearUndo is invoked
            self.performSelector_withObject_afterDelay_(
                "_clearChanges", self, 0)
            textview.setSelectedRange_(fn.NSRange(0, 0))
            self.update_syntaxer()

    @untested
    def prepareSavePanel_(self, panel):
        try:
            panel.setCanSelectHiddenExtension_(True)
            panel.setExtensionHidden_(False)
            panel.setAllowsOtherFileTypes_(True)
            name = panel.nameFieldStringValue()  # 10.6 API
            url = self.fileURL()
            if url is not None:
                filename = url.lastPathComponent()
                directory = url.URLByDeletingLastPathComponent()
                panel.setDirectoryURL_(directory)  # 10.6 API
            else:
                filename = name
                name += ".txt"
            if name != filename or (name.endswith(".txt")
                                    and "." in name[:-4]):
                panel.setNameFieldStringValue_(filename)
                exts = ["txt"]
                if "." in filename:
                    ext = filename.rsplit(".", 1)[1]
                    if ext not in exts:
                        exts.insert(0, ext)
                panel.setAllowedFileTypes_(exts)
        except Exception:
            log.error("cannot prepare save panel...", exc_info=True)
        return True

    def _clearChanges(self):
        self.updateChangeCount_(ak.NSChangeCleared)

    def icon(self):
        url = self.fileURL()
        key = "" if url is None else url.path()
        old_key, data = self.icon_cache
        if old_key is None or old_key != key:
            data = fetch_icon(key)
            self.icon_cache = (key, data)
        return data

    @property
    def comment_token(self):
        return self.syntaxer.syntaxdef.comment_token

    def _get_syntaxdef(self):
        return self.syntaxer.syntaxdef

    def _set_syntaxdef(self, value):
        self.syntaxer.syntaxdef = value
        self.syntaxer.color_text(self.text_storage)

    syntaxdef = property(_get_syntaxdef, _set_syntaxdef)

    def update_syntaxer(self):
        if self.text_storage.delegate() is not self:
            self.text_storage.setDelegate_(self)
        filename = self.lastComponentOfFileName()
        if filename != self.syntaxer.filename:
            self.syntaxer.filename = filename
            syntaxdef = app.syntax_factory.get_definition(filename)
            if self.syntaxdef is not syntaxdef:
                self.props.syntaxdef = syntaxdef
                self.syntaxer.color_text(self.text_storage)

    def textStorageDidProcessEditing_(self, notification):
        range = self.text_storage.editedRange()
        self.syntaxer.color_text(self.text_storage, range)

    def updateChangeCount_(self, ctype):
        super(TextDocument, self).updateChangeCount_(ctype)
        app.item_changed(self, ctype)

#     def set_primary_window_controller(self, wc):
#         if wc.document() is self:
#             if self.scroll_view not in wc.mainView.subviews():
#                 wc.setDocument_(self)
#         else:
#             self.addWindowController_(wc)
#         if wc.controller.find_project_with_document(self) is None:
#             wc.add_document(self)

    def __repr__(self):
        return "<%s 0x%x %s>" % (type(self).__name__, id(self),
                                 self.displayName())

    def close(self):
        # remove window controllers here so NSDocument does not close the windows
        for wc in list(self.windowControllers()):
            self.removeWindowController_(wc)
        ts = self.text_storage
        if ts is not None and ts.delegate() is self:
            ts.setDelegate_(None)
        self.text_storage = None
        super(TextDocument, self).close()
Example #7
0
def test_SyntaxCache_syntaxdef_default():
    syn = SyntaxCache()
    eq_(syn.syntaxdef, PLAIN_TEXT) # check default
    eq_(syn.filename, None) # check default
Example #8
0
class TextDocument(ak.NSDocument):

    @classmethod
    def get_with_path(cls, path):
        """Get a document with the given path

        Documents returned by this method have been added to the document
        controllers list of documents.
        """
        url = fn.NSURL.fileURLWithPath_(path)
        dc = ak.NSDocumentController.sharedDocumentController()
        doc = dc.documentForURL_(url)
        if doc is None:
            if os.path.exists(path):
                doctype, err = dc.typeForContentsOfURL_error_(url, None)
                doc, err = dc.makeDocumentWithContentsOfURL_ofType_error_(
                    url, doctype, None)
                if err is not None:
                    raise Error(err.localizedFailureReason())
                if doc is None:
                    raise Error("could not open document: %s" % path)
                dc.addDocument_(doc)
            else:
                doc, err = dc.makeUntitledDocumentOfType_error_(
                    const.TEXT_DOCUMENT, None)
                doc.setFileURL_(url)
                doc.update_syntaxer()
        return doc

    def init(self):
        super(TextDocument, self).init()
        self.setUndoManager_(UndoManager.alloc().init())
        self.id = next(doc_id_gen)
        self.icon_cache = (None, None)
        self.document_attrs = {
            ak.NSDocumentTypeDocumentAttribute: ak.NSPlainTextDocumentType,
            ak.NSCharacterEncodingDocumentAttribute: fn.NSUTF8StringEncoding,
        }
        self.text_storage = ak.NSTextStorage.alloc().initWithString_attributes_("", {})
        self.syntaxer = SyntaxCache()
        self._filestat = None
        self.props = KVOProxy(self)
        self.indent_mode = app.config["indent.mode"]
        self.indent_size = app.config["indent.size"] # should come from syntax definition
        self.newline_mode = app.config["newline_mode"]
        self.highlight_selected_text = app.config["highlight_selected_text.enabled"]
        self.reset_text_attributes(self.indent_size)
        #self.save_hooks = []
        return self

    def properties(self):
        return self.props

    def setProperties_(self, value):
        pass

    @property
    def text(self):
        return self.text_storage.mutableString()
    @text.setter
    def text(self, value):
        self.text_storage.mutableString().setString_(value)
        self.reset_text_attributes(self.indent_size)

    @property
    def newline_mode(self):
        return self._newline_mode
    @newline_mode.setter
    def newline_mode(self, value):
        self._newline_mode = value
        self.eol = const.EOLS[value]

    @property
    def character_encoding(self):
        return self.document_attrs.get(ak.NSCharacterEncodingDocumentAttribute)
    @character_encoding.setter
    def character_encoding(self, value):
        # TODO when value is None encoding should be removed from document_attrs
        if value is not None:
            self.document_attrs[ak.NSCharacterEncodingDocumentAttribute] = value
        else:
            self.document_attrs.pop(ak.NSCharacterEncodingDocumentAttribute, None)

    def reset_text_attributes(self, indent_size):
        font = ak.NSFont.fontWithName_size_("Monaco", 10.0)
        spcw = font.screenFontWithRenderingMode_(ak.NSFontDefaultRenderingMode) \
            .advancementForGlyph_(ord(" ")).width
        ps = ak.NSParagraphStyle.defaultParagraphStyle().mutableCopy()
        ps.setTabStops_([])
        ps.setDefaultTabInterval_(spcw * indent_size)
        ps = ps.copy()
        self._text_attributes = attrs = {
            ak.NSFontAttributeName: font,
            ak.NSParagraphStyleAttributeName: ps,
        }
        range = fn.NSMakeRange(0, self.text_storage.length())
        self.text_storage.addAttributes_range_(attrs, range)
        for view in app.iter_views_of_document(self):
            if view.text_view is not None:
                view.text_view.setTypingAttributes_(attrs)
                view.text_view.setDefaultParagraphStyle_(ps)

    def default_text_attributes(self):
        return self._text_attributes

    def makeWindowControllers(self):
        editor = app.current_editor()
        if editor is None:
            editor = app.create_editor()
        view = TextDocumentView.create_with_document(self)
        editor.add_document_view(view)
        self.addWindowController_(editor.wc)
        editor.current_view = view

    def readFromData_ofType_error_(self, data, doctype, error):
        success, err = self.read_data_into_textstorage(data, self.text_storage)
        if success:
            self.analyze_content()
        return (success, err)

    def read_data_into_textstorage(self, data, text_storage):
        options = {ak.NSDefaultAttributesDocumentOption: self.default_text_attributes()}
        options.update(self.document_attrs)
        while True:
            success, attrs, err = text_storage \
                .readFromData_options_documentAttributes_error_(
                    data, options, None, None)
            if success or ak.NSCharacterEncodingDocumentAttribute not in options:
                if success:
                    self.document_attrs = attrs
                break
            if err:
                log.error(err)
            options.pop(ak.NSCharacterEncodingDocumentAttribute, None)
        return success, err

    def dataOfType_error_(self, doctype, error):
        range = fn.NSMakeRange(0, self.text_storage.length())
        attrs = self.document_attrs
        data, err = self.text_storage \
            .dataFromRange_documentAttributes_error_(range, attrs, None)
        if err is None:
            try:
                self.update_syntaxer()
                app.save_editor_states()
            except Exception:
                log.error("unexpected error", exc_info=True)
#             if self.project is not None:
#                 self.project.save()
#                 self.updateSyntaxer()
#             if self.text_view is not None:
#                 # make the undo manager recognize edits after save
#                 self.text_view.breakUndoCoalescing()
        return (data, err)

    def setFileModificationDate_(self, date):
        super(TextDocument, self).setFileModificationDate_(date)
        self._filestat = None

    def analyze_content(self):
        text = self.text_storage.string()
        start, end, cend = text.getLineStart_end_contentsEnd_forRange_(
            None, None, None, (0, 0))
        if end != cend:
            eol = EOLREF.get(text[cend:end], const.NEWLINE_MODE_UNIX)
            self.newline_mode = eol
        mode, size = calculate_indent_mode_and_size(text)
        if size is not None:
            self.indent_size = size
        if mode is not None:
            self.indent_mode = mode

    def is_externally_modified(self):
        """check if this document has been modified by another program"""
        url = self.fileURL()
        if url is not None and os.path.exists(url.path()):
            ok, mdate, err = url.getResourceValue_forKey_error_(
                None, fn.NSURLContentModificationDateKey, None)
            if ok:
                return self.fileModificationDate() != mdate
        return None

    def check_for_external_changes(self, window):
        if not self.is_externally_modified():
            return
        if self.isDocumentEdited():
            if window is None:
                return # ignore change (no gui for alert)
            stat = filestat(self.fileURL().path())
            if self._filestat == stat:
                return
            self._filestat = stat
            def callback(code):
                if code == ak.NSAlertFirstButtonReturn:
                    self.reload_document()
            alert = Alert.alloc().init()
            alert.setMessageText_("“%s” source document changed" % self.displayName())
            alert.setInformativeText_("Discard changes and reload?")
            alert.addButtonWithTitle_("Reload")
            alert.addButtonWithTitle_("Cancel")
            alert.beginSheetModalForWindow_withCallback_(window, callback)
        else:
            self.reload_document()

    @refactor("improve undo after reload - use difflib to replace changed text only")
    def reload_document(self):
        """Reload document with the given URL

        This implementation allows the user to undo beyond the reload. The
        down-side is that it may use a lot of memory if the document is very
        large.
        """
        url = self.fileURL()
        if url is None or not os.path.exists(url.path()):
            return
        undo = self.undoManager()
        undo.should_remove = False
        textstore = self.text_storage
        self.text_storage = ak.NSTextStorage.alloc().init()
        try:
            ok, err = self.revertToContentsOfURL_ofType_error_(
                url, self.fileType(), None)
        finally:
            tempstore = self.text_storage
            self.text_storage = textstore
            undo.should_remove = True
        if not ok:
            log.error("could not reload document: %s", err)
            return # TODO report err
        textview = None
        for view in app.iter_views_of_document(self):
            textview = view.text_view
            if textview is not None:
                break
        text = tempstore.string()
        range = fn.NSRange(0, textstore.length())
        if textview is None:
            textstore.replaceCharactersInRange_withString_(range, text)
            undo.removeAllActions()
        elif textview.shouldChangeTextInRange_replacementString_(range, text):
            #state = self.documentState
            textstore.replaceCharactersInRange_withString_(range, text)
            #self.documentState = state
            textview.didChangeText()
            textview.breakUndoCoalescing()
            # HACK use timed invocation to allow didChangeText notification
            # to update change count before _clearUndo is invoked
            self.performSelector_withObject_afterDelay_("_clearChanges", self, 0)
            textview.setSelectedRange_(fn.NSRange(0, 0))
            self.update_syntaxer()

    @untested
    def prepareSavePanel_(self, panel):
        try:
            panel.setCanSelectHiddenExtension_(True)
            panel.setExtensionHidden_(False)
            panel.setAllowsOtherFileTypes_(True)
            name = panel.nameFieldStringValue() # 10.6 API
            url = self.fileURL()
            if url is not None:
                filename = url.lastPathComponent()
                directory = url.URLByDeletingLastPathComponent()
                panel.setDirectoryURL_(directory) # 10.6 API
            else:
                filename = name
                name += ".txt"
            if name != filename or (name.endswith(".txt") and "." in name[:-4]):
                panel.setNameFieldStringValue_(filename)
                exts = ["txt"]
                if "." in filename:
                    ext = filename.rsplit(".", 1)[1]
                    if ext not in exts:
                        exts.insert(0, ext)
                panel.setAllowedFileTypes_(exts)
        except Exception:
            log.error("cannot prepare save panel...", exc_info=True)
        return True

    def _clearChanges(self):
        self.updateChangeCount_(ak.NSChangeCleared)

    def icon(self):
        url = self.fileURL()
        key = "" if url is None else url.path()
        old_key, data = self.icon_cache
        if old_key is None or old_key != key:
            data = fetch_icon(key)
            self.icon_cache = (key, data)
        return data

    @property
    def comment_token(self):
        return self.syntaxer.syntaxdef.comment_token

    def _get_syntaxdef(self):
        return self.syntaxer.syntaxdef
    def _set_syntaxdef(self, value):
        self.syntaxer.syntaxdef = value
        self.syntaxer.color_text(self.text_storage)
    syntaxdef = property(_get_syntaxdef, _set_syntaxdef)

    def update_syntaxer(self):
        if self.text_storage.delegate() is not self:
            self.text_storage.setDelegate_(self)
        filename = self.lastComponentOfFileName()
        if filename != self.syntaxer.filename:
            self.syntaxer.filename = filename
            syntaxdef = app.syntax_factory.get_definition(filename)
            if self.syntaxdef is not syntaxdef:
                self.props.syntaxdef = syntaxdef
                self.syntaxer.color_text(self.text_storage)

    def textStorageDidProcessEditing_(self, notification):
        range = self.text_storage.editedRange()
        self.syntaxer.color_text(self.text_storage, range)

    def updateChangeCount_(self, ctype):
        super(TextDocument, self).updateChangeCount_(ctype)
        app.item_changed(self, ctype)

#     def set_primary_window_controller(self, wc):
#         if wc.document() is self:
#             if self.scroll_view not in wc.mainView.subviews():
#                 wc.setDocument_(self)
#         else:
#             self.addWindowController_(wc)
#         if wc.controller.find_project_with_document(self) is None:
#             wc.add_document(self)

    def __repr__(self):
        return "<%s 0x%x %s>" % (type(self).__name__, id(self), self.displayName())

    def close(self):
        # remove window controllers here so NSDocument does not close the windows
        for wc in list(self.windowControllers()):
            self.removeWindowController_(wc)
        ts = self.text_storage
        if ts is not None and ts.delegate() is self:
            ts.setDelegate_(None)
        self.text_storage = None
        super(TextDocument, self).close()