Пример #1
0
 def setup(view, syntax_control, get_syntax):
     ALL = object()
     edit_range = None
     highlighter = Highlighter(self.app.theme)
     highlighter.syntaxdef = get_syntax()
     view.highlighter = highlighter
     def coalesce_edit_ranges(rng=None):
         nonlocal edit_range
         rng_ = edit_range
         if rng is None or rng_ is ALL:
             rng = None
         elif rng_ is not None:
             rng = union_range(rng, rng_)
         edit_range = rng or ALL
         return (rng,), {}
     @debounce(0.05, coalesce_edit_ranges)
     def color_text(rng):
         nonlocal edit_range
         highlighter.color_text(view.text, rng, timeout=0.05)
         edit_range = None
     def text_changed(rng):
         if view.text.editedMask() == ak.NSTextStorageEditedAttributes:
             # break color text -> attribute change -> color text -> ...
             return
         color_text(rng)
     view.text.on_edit(text_changed)
     def change_syntax():
         syntaxdef = get_syntax()
         if highlighter.syntaxdef is not syntaxdef:
             highlighter.syntaxdef = syntaxdef
             color_text()
     syntax_control.on.click(change_syntax)
Пример #2
0
 def edit(lang, string, expect, *pairs):
     hl = Highlighter(theme)
     hl.syntaxdef = get_syntax_definition(lang)
     assert len(pairs) % 2 == 0, "got odd number of edit/expect pairs"
     string = dedent(string) if string.startswith("\n") else string
     text = Text(string)
     yield test(hl, text, expect)
     for edit, expect in zip(islice(pairs, 0, None, 2), islice(pairs, 1, None, 2)):
         edit_text = dedent(edit[2]).lstrip() if edit[2].startswith("\n") else edit[2]
         edit = edit[0], edit[1], edit_text
         yield test(hl, text, expect, edit)
Пример #3
0
    def __init__(self, app, path=None):
        self.app = app
        self._updates_path_on_file_move = app.config["updates_path_on_file_move"]
        self.file_path = path or const.UNTITLED_DOCUMENT_NAME
        self.persistent_path = path
        self.id = next(DocumentController.id_gen)
        self.icon_cache = (None, None)
        self.document_attrs = {
            ak.NSDocumentTypeDocumentAttribute: ak.NSPlainTextDocumentType,
            ak.NSCharacterEncodingDocumentAttribute: fn.NSUTF8StringEncoding,
        }
        self.undo_manager = UndoManager()
        self.syntaxer = Highlighter(app.theme)
        self.props = KVOProxy(self)
        self._kvo = KVOLink([
            (self.undo_manager, "has_unsaved_actions", self.props, "is_dirty"),
        ])
        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["theme.highlight_selected_text.enabled"]
        self.syntax_needs_color = False
        self._edit_range = None

        app.on_reload_config(self.reset_text_attributes, self)
Пример #4
0
 def test(lang, string, expect, edit=None):
     theme.reset()
     if isinstance(lang, Highlighter):
         hl = lang
     else:
         hl = Highlighter(theme)
         hl.syntaxdef = get_syntax_definition(lang)
     text = string if isinstance(string, Text) else Text(dedent(string))
     get_color = lambda value: value
     with replattr(editxt.theme, "get_color", get_color, sigcheck=False), CaptureLogging(mod) as log:
         if edit:
             start, length, insert = edit
             text[(start, length)] = Text(insert)
             hl.color_text(text, (start, len(insert)))
         else:
             hl.color_text(text)
     errors = log.data.get("error")
     assert not errors, "Errors logged:\n" + "\n---\n\n".join(errors)
     a = list(chain.from_iterable(x.split("\n") for x in text.colors(hl)))
     b = dedent(expect).strip().split("\n") if expect else []
     assert a == b, "\n" + "\n".join(unified_diff(b, a, "expected", "actual", lineterm=""))
Пример #5
0
def test_Highlighter_color_text():
    from objc import NULL
    from textwrap import dedent
    from editxt.platform.text import Text as BaseText
    from editxt.syntax import SYNTAX_RANGE, SYNTAX_TOKEN

    class Text(BaseText):
        def colors(self, highlighter):
            seen = set()
            lines = []
            length = len(self)
            full = rng = (0, length)
            fg_color = ak.NSForegroundColorAttributeName
            long_range = self.attribute_atIndex_longestEffectiveRange_inRange_
            while rng[1] > 0:
                key, xrng = long_range(SYNTAX_RANGE, rng[0], None, rng)
                lang = highlighter.langs[key].name if key else ""
                if lang == highlighter.syntaxdef.name:
                    lang = ""
                # print(lang.strip(), xrng)
                level = 1 if lang else 0
                xend = sum(xrng)
                while True:
                    attr, attr_rng = long_range(SYNTAX_TOKEN, xrng[0], None, full)
                    color, color_rng = long_range(fg_color, xrng[0], None, full)
                    if attr:
                        color_rng = attr_rng
                    else:
                        color_rng = intersect_ranges(attr_rng, color_rng)
                    text = self[color_rng]
                    if attr or color and color != "text_color" and text.strip():
                        if attr:
                            attr_name = attr
                        else:
                            # default text color, not a matched token
                            attr_name = "{} {} -".format(lang, color)
                        print(attr_name.ljust(30), (key or "").ljust(15), repr(text)[1:-1])
                        print_lang = lang and attr and attr.startswith(lang)
                        attr = attr.rsplit(" ", 1)[-1] if attr else color
                        lines.append(
                            "{}{} {}{}".format(
                                "  " * level,
                                text,
                                attr if attr == color else "{} {}".format(attr, color),
                                (" " + lang) if print_lang else "",
                            )
                        )
                    start = sum(attr_rng)
                    if start >= xend:
                        xend = start
                        break
                    xrng = (start, xend - start)
                rng = (xend, length - xend)
            return lines

    @gentest
    def test(lang, string, expect, edit=None):
        theme.reset()
        if isinstance(lang, Highlighter):
            hl = lang
        else:
            hl = Highlighter(theme)
            hl.syntaxdef = get_syntax_definition(lang)
        text = string if isinstance(string, Text) else Text(dedent(string))
        get_color = lambda value: value
        with replattr(editxt.theme, "get_color", get_color, sigcheck=False), CaptureLogging(mod) as log:
            if edit:
                start, length, insert = edit
                text[(start, length)] = Text(insert)
                hl.color_text(text, (start, len(insert)))
            else:
                hl.color_text(text)
        errors = log.data.get("error")
        assert not errors, "Errors logged:\n" + "\n---\n\n".join(errors)
        a = list(chain.from_iterable(x.split("\n") for x in text.colors(hl)))
        b = dedent(expect).strip().split("\n") if expect else []
        assert a == b, "\n" + "\n".join(unified_diff(b, a, "expected", "actual", lineterm=""))

    def edit(lang, string, expect, *pairs):
        hl = Highlighter(theme)
        hl.syntaxdef = get_syntax_definition(lang)
        assert len(pairs) % 2 == 0, "got odd number of edit/expect pairs"
        string = dedent(string) if string.startswith("\n") else string
        text = Text(string)
        yield test(hl, text, expect)
        for edit, expect in zip(islice(pairs, 0, None, 2), islice(pairs, 1, None, 2)):
            edit_text = dedent(edit[2]).lstrip() if edit[2].startswith("\n") else edit[2]
            edit = edit[0], edit[1], edit_text
            yield test(hl, text, expect, edit)

    config = Config(
        None,
        schema={
            "theme": {
                "text_color": String("text_color"),
                "syntax": {
                    "default": {
                        "attribute": String("attribute"),
                        "builtin": String("builtin"),
                        "comment": String("comment"),
                        "group": String("group"),
                        "header": String("header"),
                        "keyword": String("keyword"),
                        "name": String("name"),
                        "operator": String("operator"),
                        "punctuation": String("punctuation"),
                        "string": String("string"),
                        "string.multiline.single-quote": String("string.multiline.single-quote"),
                        "tag": String("tag"),
                        "value": String("value"),
                    },
                    "JavaScript": {"string": String("js.string")},
                },
            }
        },
    )
    theme = editxt.theme.Theme(config)

    text = Text("def f(self, x): x # TODO")
    hl = Highlighter(theme)
    hl.syntaxdef = get_syntax_definition("python")
    hl.color_text(text)
    yield test(mod.PLAIN_TEXT, text, "")

    yield test(
        "python",
        "def f(self, x): x # TODO",
        """
        def keyword
        self builtin
        # TODO comment.single-line comment
        """,
    )
    yield test(
        "python",
        "# \U0001f612 \U0001f34c\ndef f(self, x): x # TODO",
        """
        # \U0001f612 \U0001f34c comment.single-line comment
        def keyword
        self builtin
        # TODO comment.single-line comment
        """,
    )
    yield test(
        "python",
        "'\nfor x",
        """
        ' string.single-quote string
        for keyword
        """,
    )
    yield test(
        "python",
        '"\ndef f(',
        """
        " string.double-quote string
        def keyword
        """,
    )
    yield test(
        "python",
        r"""
        x = "\""
        y = "\\"
        z = "\\\""
        Q = "\\\\\""
        """,
        r"""
        " string.double-quote string
        \" operator.escape operator
        " string.double-quote string
        " string.double-quote string
        \\ operator.escape operator
        " string.double-quote string
        " string.double-quote string
        \\\" operator.escape operator
        " string.double-quote string
        " string.double-quote string
        \\\\\" operator.escape operator
        " string.double-quote string
        """,
    )
    yield test(
        "python",
        r"""
        x = r"\""
        y = r"\\"
        z = r"\\\""
        Q = r"\\\\\""
        """,
        r"""
        r" string.double-quote string
          \" operator.escape operator Regular Expression
          " string.double-quote string
        r" string.double-quote string
          \\ operator.escape operator Regular Expression
          " string.double-quote string
        r" string.double-quote string
          \\\" operator.escape operator Regular Expression
          " string.double-quote string
        r" string.double-quote string
          \\\\\" operator.escape operator Regular Expression
          " string.double-quote string
        """,
    )
    yield test(
        "python",
        "'''    for x",
        """
        ''' string.multiline.single-quote
            for x string.multiline.single-quote
        """,
    )
    yield test(
        "python",
        "'''    for x'''",
        """
        ''' string.multiline.single-quote
            for x string.multiline.single-quote
        ''' string.multiline.single-quote
        """,
    )
    yield test(
        "python",
        "'begin\\\nend'",
        """
        ' string.single-quote string
        begin string
        \\
         operator.escape.continuation operator
        end string
        ' string.single-quote string
        """,
    )
    yield test(
        "python",
        '''"""A doc string\nWith multiple lines\n"""''',
        '''
        """ string.multiline.double-quote string
        A doc string
        With multiple lines
         string
        """ string.multiline.double-quote string
        ''',
    )
    yield from edit(
        "python",
        "\ndef f(",
        """
        def keyword
        """,
        (0, 0, '"'),
        """
        " string.double-quote string
        def keyword
        """,
        (1, 0, '"'),
        """
        "" string.double-quote string
        def keyword
        """,
        (2, 0, '"'),
        '''
        """ string.multiline.double-quote string
        
        def f( string
        ''',
        (2, 1, ""),
        """
        "" string.double-quote string
        def keyword
        """,
    )
    yield from edit(
        "python",
        "def\n",
        """
        def keyword
        """,
        (0, 4, ""),  # bug: error on delete all content
        "",
    )
    yield from edit(
        "python",
        ' "word" ',
        """
        " string.double-quote string
        word string
        " string.double-quote string
        """,
        (1, 0, "r"),
        """
        r" string.double-quote string
          word string
          " string.double-quote string
        """,
        (1, 1, ""),
        """
        " string.double-quote string
        word string
        " string.double-quote string
        """,
    )
    yield from edit(
        "python",
        r"""r"(?P<xyz>)" """,
        """
        r" string.double-quote string
          (?P< group.named group Regular Expression
          xyz name Regular Expression
          > group.named group Regular Expression
          ) group Regular Expression
          " string.double-quote string
        """,
        (4, 0, " "),
        """
        r" string.double-quote string
          ( string
          ? keyword Regular Expression
           P<xyz> string
          ) group Regular Expression
          " string.double-quote string
        """,
        (4, 1, ""),
        """
        r" string.double-quote string
          (?P< group.named group Regular Expression
          xyz name Regular Expression
          > group.named group Regular Expression
          ) group Regular Expression
          " string.double-quote string
        """,
    )
    yield from edit(
        "python",
        '''
        r""" [\s] """
        []
        ' """ """ '
        ''',
        '''
        r""" string.multiline.double-quote string
          [ keyword.set keyword Regular Expression
          \s operator.class operator Regular Expression
          ] keyword.set keyword Regular Expression
          """ string.multiline.double-quote string
        ' string.single-quote string
         """ """  string
        ' string.single-quote string
        ''',
        (9, 1, ""),
        '''
        r""" string.multiline.double-quote string
          [ keyword.set keyword Regular Expression
          \s operator.class operator Regular Expression
          """ string.multiline.double-quote string
        ' string.single-quote string
         """ """  string
        ' string.single-quote string
        ''',
        (9, 0, "]"),
        '''
        r""" string.multiline.double-quote string
          [ keyword.set keyword Regular Expression
          \s operator.class operator Regular Expression
          ] keyword.set keyword Regular Expression
          """ string.multiline.double-quote string
        ' string.single-quote string
         """ """  string
        ' string.single-quote string
        ''',
    )
    yield from edit(
        "python",
        '''
            """x
            """
        def
        ''',
        '''
        """ string.multiline.double-quote string
        x
             string
        """ string.multiline.double-quote string
        def keyword
        ''',
        (16, 0, "\n"),
        '''
        """ string.multiline.double-quote string
        x
             string
        """ string.multiline.double-quote string
        def keyword
        ''',
    )

    yield test(
        "markup",
        """<div""",
        """
        <div tag
        """,
    )
    yield test(
        "markup",
        """<div <span>""",
        """
        <div tag
        <span> tag
        """,
    )
    yield test(
        "markup",
        """<div tal:wrap='<span>'""",
        """
        <div tag
        tal:wrap attribute
        = tag.punctuation tag
        '<span>' value
        """,
    )
    yield test(
        "markup",
        """<div class='ext' data id="xpr"> </div>""",
        """
        <div tag
        class attribute
        = tag.punctuation tag
        'ext' value
        data attribute
        id attribute
        = tag.punctuation tag
        "xpr" value
        > tag
        </div> tag
        """,
    )
    yield test(
        "markup",
        """<!DOCTYPE>\n<div/>""",
        """
        <!DOCTYPE> tag.doctype tag
        <div/> tag
        """,
    )
    yield test(
        "markup",
        """<!DOCTYPE html encoding="utf-8">\n<div/>""",
        """
        <!DOCTYPE tag.doctype tag
        html attribute
        encoding attribute
        = tag.punctuation tag
        "utf-8" value
        > tag.doctype tag
        <div/> tag
        """,
    )
    yield test("markup", """<!---->""", """<!----> comment""")
    yield test("markup", """<!-- <head><style> -->""", """<!-- <head><style> --> comment""")
    yield test(
        "markup",
        """<div><![CDATA[ abc <xyz> 3&""",
        """
        <div> tag
        <![CDATA[ tag.cdata tag
        """,
    )
    yield test(
        "markup",
        """<div><![cdata[ abc <xyz> 3& ]]>""",
        """
        <div> tag
        <![cdata[ tag.cdata tag
        ]]> tag.cdata tag
        """,
    )
    yield test(
        "markup",
        "<style attr='value'></style>",
        """
        <style tag
        attr attribute
        = tag.punctuation tag
        'value' value
        ></style> tag
        """,
    )
    yield test(
        "markup",
        "<script>var x = 'y';</script>",
        """
        <script> tag
          var keyword JavaScript
          'y' string.single-quote js.string JavaScript
          </script> tag
        """,
    )
    yield test(
        "markup",
        "<style>.error { color: red; }</style>",
        """
        <style> tag
          .error selector-class text_color CSS
          { text_color CSS
          color attribute CSS
          : text_color CSS
          ; text_color CSS
          } text_color CSS
          </style> tag
        """,
    )

    yield test(
        "markdown",
        """
        ```Python
        def inc(arg):
            return arg + 1
        ```
        """,
        """
        ``` code text_color
        Python tag
          def keyword Python
          return keyword Python
          ``` code text_color
        """,
    )

    yield test(
        "markdown",
        """
        ```unknown-language-name
        def inc(arg):
            return arg + 1
        ```
        """,
        """
        ``` code text_color
        unknown-language-name text_color
        ``` code text_color
        """,
    )

    yield test(
        "markdown",
        """
        *Clojure REPL*

        ```clojure-repl
        user=> (defn f [x y]
          #_=>   (+ x y))
        #'user/f
        user=> (f 5 7)
        12
        user=> nil
        nil
        ```

        *Clojure*
        """,
        """
        *Clojure REPL* emphasis text_color
        ``` code text_color
        clojure-repl tag
          user=> meta text_color Clojure REPL
          ( text_color Clojure
          defn builtin-name text_color Clojure
          [ text_color Clojure
          ] text_color Clojure
            #_=> meta text_color Clojure REPL
          ( text_color Clojure
          + builtin-name text_color Clojure
          ) text_color Clojure
          user=> meta text_color Clojure REPL
          ( text_color Clojure
          f name Clojure
          5 number text_color Clojure
          7 number text_color Clojure
          ) text_color Clojure
          user=> meta text_color Clojure REPL
          nil literal text_color Clojure
          ``` code text_color
        *Clojure* emphasis text_color
        """,
    )
    yield test(
        "markdown",
        """
        ```asciidoc
        Want to see a image::images/tiger.png[Tiger]?
        *strong*
        [quote, Sir Arthur Conan Doyle]
        ____
        When you have eliminated all...
        ```
        not *strong*
        """,
        """
        ``` code text_color
        asciidoc tag
          image::images/tiger.png link text_color AsciiDoc
          [ text_color AsciiDoc
          Tiger string
          ] text_color AsciiDoc
          *strong* strong text_color AsciiDoc
          [quote, Sir Arthur Conan Doyle] meta text_color AsciiDoc
          ____
        When you have eliminated all... quote text_color AsciiDoc
          ``` code text_color
        *strong* emphasis text_color
        """,
    )
    yield test(
        "markdown",
        """
        ## YAML

        ```yaml
        ---
        # comment
        string_1: "Bar"
        """,
        """
        ## YAML section text_color
        ``` code text_color
        yaml tag
          --- meta text_color YAML
          # comment YAML
           comment comment
          string_1: attr text_color YAML
          " string YAML
          Bar string
          " string YAML
        """,
    )

    yield test(
        "javascript",
        "var x = 'y';",
        """
        var keyword
        'y' string.single-quote js.string
        """,
    )
    yield test(
        "javascript",
        "var regex = /y*/ig;",
        """
        var keyword
        / regexp text_color
          * keyword Regular Expression
          /ig regexp text_color
        """,
    )
    yield test(
        "javascript",
        "/[x-z/ig - var;",
        """
        / regexp text_color
          [ keyword.set keyword Regular Expression
          - operator.range operator Regular Expression
          /ig regexp text_color
        var keyword
        """,
    )

    yield test(
        "regular-expression",
        r"^a(.)c$",
        """
        ^ keyword
        ( group
        . keyword
        ) group
        $ keyword
        """,
    )

    yield test(
        "regular-expression",
        r"a(?:(.))c",
        """
        (?:( group
        . keyword
        )) group
        """,
    )

    yield test(
        "regular-expression",
        r" good (?# junk ) good (?=aft.r)(?!n.t)",
        """
        (?# junk ) comment
        (?= group
        . keyword
        )(?! group
        . keyword
        ) group
        """,
    )

    yield test(
        "regular-expression",
        r"(?<=b.hind)(?<!n.t)",
        """
        (?<= group
        . keyword
        )(?<! group
        . keyword
        ) group
        """,
    )

    yield test(
        "regular-expression",
        r"(.)?abc(?(1).|$)",
        """
        ( group
        . keyword
        ) group
        ? keyword
        (?(1) group
        .|$ keyword
        ) group
        """,
    )

    yield test(
        "regular-expression",
        r"\1 \01 \7654 \999",
        r"""
        \1 group.ref group
        \01 operator.escape.char operator
        \765 operator.escape.char operator
        \99 group.ref group
        """,
    )

    yield test(
        "regular-expression",
        r"\A \b \B \w \W \Z \\ \. \u0a79 \U000167295",
        r"""
        \A operator.class operator
        \b operator.class operator
        \B operator.class operator
        \w operator.class operator
        \W operator.class operator
        \Z operator.class operator
        \\ operator.escape operator
        \. operator.escape operator
        \u0a79 operator.escape.char operator
        \U00016729 operator.escape.char operator
        """,
    )

    yield test(
        "regular-expression",
        r"[-\wa-z\-\S-]",
        r"""
        [ keyword.set keyword
        \w operator.class operator
        - operator.range operator
        \- operator.escape operator
        \S operator.class operator
        ] keyword.set keyword
        """,
    )

    yield test(
        "regular-expression",
        r" [^-\wa-z\-\S-] ",
        r"""
        [^ keyword.set.inverse keyword
        \w operator.class operator
        - operator.range operator
        \- operator.escape operator
        \S operator.class operator
        ] keyword.set.inverse keyword
        """,
    )

    yield test(
        "erlang",
        """
        -module(ssh_cli).

        -behaviour(ssh_channel).

        -include("ssh.hrl").
        %% backwards compatibility
        """,
        """
        -module keyword
        ( params text_color
        ) params text_color
        . text_color
        -behaviour keyword
        ( params text_color
        ) params text_color
        . text_color
        -include keyword
        ( params text_color
        " string
        ssh.hrl string
        " string
        ) params text_color
        . text_color
        % comment
        % backwards compatibility comment
        """,
    )

    yield test(
        "erlang",
        """
        init([Shell, Exec]) ->
            {ok, #state{shell = Shell, exec = Exec}};
        """,
        """
        init title text_color
        ( params text_color
        ) params text_color
        -> function text_color
        { text_color
        #state{ text_color
        }} text_color
        ; function text_color
        """,
    )

    yield from edit(
        "YAML",
        """
        #theme:
        #    text_color: EFEFEF
        #    selection_color: 584848
        #    background_color: 101010


        font:
          face: Inconsolata
          size: 13
        """,
        """
        # comment
        theme: comment
        # comment
            text_color: EFEFEF comment
        # comment
            selection_color: 584848 comment
        # comment
            background_color: 101010 comment
        font: attr text_color
          face: attr text_color
          size: attr text_color
        13 number text_color
        """,
        (
            93,
            0,
            """
        shortcuts:
          "Command+{": " doc  down"
          "Command+}": " doc  up"
        """,
        ),
        """
        # comment
        theme: comment
        # comment
            text_color: EFEFEF comment
        # comment
            selection_color: 584848 comment
        # comment
            background_color: 101010 comment
        shortcuts: attr text_color
        " string
        Command+{ string
        " string
        " string
         doc  down string
        " string
        " string
        Command+} string
        " string
        " string
         doc  up string
        " string
        font: attr text_color
          face: attr text_color
          size: attr text_color
        13 number text_color
        """,
    )

    class lang:
        class sub:
            rules = [("keyword", ["for"])]

        rules = [("keyword", ["for", "in"]), ("tag", "[", ["]"], sub)]

    lang = make_definition(lang)
    yield from edit(
        lang,
        "for x in [y for y in z]:",
        """
        for keyword
        in keyword
        [ tag
        for keyword
        ] tag
        """,
        (9, 1, ""),
        """
        for keyword
        in keyword
        for keyword
        in keyword
        """,
    )
    yield from edit(
        lang,
        "for x in [y for y in z]:",
        """
        for keyword
        in keyword
        [ tag
        for keyword
        ] tag
        """,
        (4, 6, "[x for "),
        """
        for keyword
        [ tag
        for keyword
        for keyword
        ] tag
        """,
    )

    class xkw:
        rules = [("name", ["x"])]

    class lang:
        name = "transition"

        class params:
            rules = [("group", "(", [")"], xkw)]

        class func_name:
            rules = [("name", [RE(r"[a-z]+")])]

        rules = [
            ("keyword", ["end"]),
            ("keyword", "def", [RE(r"$"), params], func_name),
            ("tag", RE(r"[a-z]+\("), [")"], xkw),
        ]

    lang = make_definition(lang)
    yield test(
        lang,
        "def f(x) x + do(x + 1) end",
        """
        def keyword
        f name
        ( group
        x name
        ) group
        do( tag
        x name
        ) tag
        end keyword
        """,
    )

    class lang:
        name = "recursive"

        class call:
            rules = [("tag", ["do"])]

        rules = [("keyword", ["def"]), ("group", "(", [")"], call)]

    lang.call.rules.append(lang.rules[1])
    lang = make_definition(lang)
    yield test(
        lang,
        "def (def do(do (def)))",
        """
        def keyword
        ( group
        do tag
        ( group
        do tag
        ( group
        ))) group
        """,
    )

    class lang:
        name = "start-end-lang-no-body"
        default_text = const.DELIMITER

        class _lparen:
            rules = [("_lparen", ["("])]

        class _rparen:
            rules = [("_rparen", [")"])]

        rules = [("keyword", ["def"]), ("group", _lparen, [_rparen])]

    lang = make_definition(lang)
    yield test(
        lang,
        "def (do def) def",
        """
        def keyword
        ( text_color
        do def group
        ) text_color
        def keyword
        """,
    )

    def test_err(msg, *args):
        with CaptureLogging(mod) as log:
            test.test(*args)
            eq_(log.data["exception"], [Regex(msg)])

    class lang:
        name = "non-advancing-range"
        rules = [("group", RE(r"(?=.)"), [RE(r"\b|\B")])]

    lang = make_definition(lang)
    yield test_err, "non-advancing range: index=0 ", lang, "a", ""

    class lang:
        name = "infinite-language-recursion"
        rules = []

    lang.rules.append(("group", RE(r"(?=.)"), [RE(r"x")], lang))
    lang = make_definition(lang)
    yield test_err, "max recursion exceeded", lang, "a", ""
Пример #6
0
class TextDocument(object):

    app = WeakProperty()

    def __init__(self, app, path=None):
        self.app = app
        self._updates_path_on_file_move = app.config["updates_path_on_file_move"]
        self.file_path = path or const.UNTITLED_DOCUMENT_NAME
        self.persistent_path = path
        self.id = next(DocumentController.id_gen)
        self.icon_cache = (None, None)
        self.document_attrs = {
            ak.NSDocumentTypeDocumentAttribute: ak.NSPlainTextDocumentType,
            ak.NSCharacterEncodingDocumentAttribute: fn.NSUTF8StringEncoding,
        }
        self.undo_manager = UndoManager()
        self.syntaxer = Highlighter(app.theme)
        self.props = KVOProxy(self)
        self._kvo = KVOLink([(self.undo_manager, "has_unsaved_actions", self.props, "is_dirty")])
        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["theme.highlight_selected_text.enabled"]
        self.syntax_needs_color = False
        self._edit_range = None

        app.on_reload_config(self.reset_text_attributes, self)
        # self.save_hooks = []

    @property
    def name(self):
        return os.path.basename(self.file_path)

    @property
    def file_path(self):
        if self.updates_path_on_file_move:
            return self._fileref.path
        return self._fileref

    @file_path.setter
    def file_path(self, value):
        if self.updates_path_on_file_move:
            ref = getattr(self, "_fileref", None)
            if ref is None:
                old_path = None
                self._fileref = FileRef(value)
            else:
                old_path = ref.original_path
                ref.path = value
        else:
            old_path = getattr(self, "_fileref", None)
            self._fileref = value
        self._refresh_file_mtime()  # TODO should this (always) happen here?
        if self.has_real_path():
            self.app.documents.change_document_path(old_path, self)

    @property
    def updates_path_on_file_move(self):
        return self._updates_path_on_file_move

    @updates_path_on_file_move.setter
    def updates_path_on_file_move(self, value):
        path = self.file_path = self.file_path
        if value:
            self._fileref = FileRef(path)
        else:
            self._fileref = path
        self._updates_path_on_file_move = value

    @property
    def file_mtime(self):
        return self._filestat.st_mtime if self._filestat else None

    @property
    def text_storage(self):
        try:
            return self._text_storage
        except AttributeError:
            self.text_storage = Text()
            self._load()
        return self._text_storage

    @text_storage.setter
    def text_storage(self, value):
        if hasattr(self, "_disable_text_edit_callback"):
            self._disable_text_edit_callback()
            del self._disable_text_edit_callback
        if value is not None:
            self._disable_text_edit_callback = value.on_edit(self.on_text_edit)
        self._text_storage = value

    @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()

    @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)

    @property
    def font(self):
        try:
            return self._font
        except AttributeError:
            return self.app.default_font

    @font.setter
    def font(self, value):
        self._font = value
        self.reset_text_attributes()

    def reset_text_attributes(self, indent_size=0, event=None):
        attrs = self.default_text_attributes(indent_size)
        if self.text_storage is not None:
            no_color = {k: v for k, v in attrs.items() if k != ak.NSForegroundColorAttributeName}
            range = fn.NSMakeRange(0, self.text_storage.length())
            self.text_storage.addAttributes_range_(no_color, range)
            self.text_storage.setFont_(self.font.font)
            self.syntax_needs_color = event and event.theme_changed
        for editor in self.app.iter_editors_of_document(self):
            editor.set_text_attributes(attrs)

    def default_text_attributes(self, indent_size=None):
        if indent_size is None:
            try:
                return self._text_attributes
            except AttributeError:
                indent_size = self.indent_size
        if indent_size == 0:
            indent_size = self.indent_size
        font = self.font.font
        tabw = (
            ak.NSString.stringWithString_("8" * indent_size).sizeWithAttributes_({ak.NSFontAttributeName: font}).width
        )
        ps = ak.NSParagraphStyle.defaultParagraphStyle().mutableCopy()
        ps.setTabStops_([])
        ps.setDefaultTabInterval_(tabw)
        self._text_attributes = attrs = {
            ak.NSFontAttributeName: font,
            ak.NSParagraphStyleAttributeName: ps,
            ak.NSForegroundColorAttributeName: self.app.theme.text_color,
        }
        return attrs

    def _load(self):
        """Load the document's file contents from disk

        :returns: True if the file was loaded from disk, otherwise False.
        """
        self.persistent_path = self.file_path
        self._refresh_file_mtime()
        if self._filestat is not None:
            data = ak.NSData.dataWithContentsOfFile_(self.file_path)
            success, err = self.read_from_data(data)
            if success:
                self.analyze_content()
                self.reset_text_attributes()
            else:
                log.error(err)  # TODO display error in progress bar
            return success
        self.reset_text_attributes()
        return False

    def read_from_data(self, data):
        options = {ak.NSDefaultAttributesDocumentOption: self.default_text_attributes()}
        options.update(self.document_attrs)
        while True:
            success, attrs, err = self._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 save(self):
        """Save the document to disk

        :raises: Error if the document does not have a real path.
        """
        if not self.has_real_path():
            if isabs(self.file_path):
                raise Error("parent directory is missing: {}".format(self.file_path))
            else:
                raise Error("file path is not set")
        path = self.file_path
        self.write_to_file(path)
        self.file_path = path  # update FileRef
        self.persistent_path = path
        for action in [self._refresh_file_mtime, self.clear_dirty, self.app.save_window_states, self.update_syntaxer]:
            try:
                action()
            except Exception:
                log.error("unexpected error", exc_info=True)

    def write_to_file(self, path):
        """Write the document's content to the given file path

        The given file path must be an absolute path.
        """
        if not isabs(path):
            raise Error("cannot write to relative path: {}".format(path))
        data, err = self.data()
        if err is None:
            ok, err = data.writeToFile_options_error_(path, 1, None)
        if not ok:
            raise Error("cannot write to {}: {}".format(path, err))

    def data(self):
        range = fn.NSMakeRange(0, self.text_storage.length())
        attrs = self.document_attrs
        data, err = self.text_storage.dataFromRange_documentAttributes_error_(range, attrs, None)
        return (data, err)

    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.props.newline_mode = eol
        mode, size = calculate_indent_mode_and_size(text)
        if size is not None:
            self.props.indent_size = size
        if mode is not None:
            self.props.indent_mode = mode

    def has_real_path(self):
        """Return true if this docuemnt has an absolute path where it could
           possibly be saved; otherwise false

        Note that this is not a garantee that the file can be saved at its
        currently assigned file_path. For example, this will not detect if
        file system permissions would prevent writing.
        """
        return isabs(self.file_path) and isdir(dirname(self.file_path))

    def file_exists(self):
        """Return True if this file has no absolute path on disk"""
        return self.has_real_path() and exists(self.file_path)

    def _refresh_file_mtime(self):
        if self.file_exists():
            self._filestat = filestat(self.file_path)
            return
        self._filestat = None

    def is_externally_modified(self):
        """check if this document has been modified by another program

        :returns: True if the file has been modified by another program,
        otherwise False, and None if the file cannot be accessed.
        """
        if self.file_exists():
            stat = filestat(self.file_path)
            if stat is not None:
                return self._filestat != stat
        return None

    def file_changed_since_save(self):
        """Check if the file on disk has changed since the last save

        :returns: True if the file on disk has been edited or moved by an
        external program, False if the file exists but has not changed, and
        None if the file does not exist.
        """
        return self.persistent_path != self.file_path or self.is_externally_modified()

    def check_for_external_changes(self, window):
        if not self.is_externally_modified():
            return
        if self.is_dirty():
            if window is None:
                return  # ignore change (no gui for alert)
            stat = filestat(self.file_path)
            if self._filestat == stat:  # TODO is this check necessary?
                return
            self._filestat = stat

            def callback(code, end_alert):
                if code == ak.NSAlertFirstButtonReturn:
                    self.reload_document()
                # should self.file_changed_since_save() -> True on cancel here?

            alert = Alert.alloc().init()
            alert.setMessageText_("“%s” source document changed" % self.name)
            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.
        """
        if not self.file_exists():
            return
        textstore = self._text_storage
        self._text_storage = Text()
        try:
            ok = self._load()
        finally:
            tempstore = self._text_storage
            self._text_storage = textstore
        if not ok:
            return
        textview = None
        for editor in self.app.iter_editors_of_document(self):
            textview = editor.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)
            self.undo_manager.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
            call_later(0, self.clear_dirty)
            textview.setSelectedRange_(fn.NSRange(0, 0))
            self.reset_text_attributes()
            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 is_dirty(self):
        return self.undo_manager.has_unsaved_actions()

    def clear_dirty(self):
        self.undo_manager.savepoint()

    def icon(self):
        path = self.file_path
        key = "" if path is None else 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

    @comment_token.setter
    def comment_token(self, value):
        self.syntaxer.syntaxdef.comment_token = value

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

    @syntaxdef.setter
    def syntaxdef(self, value):
        self.syntaxer.syntaxdef = value
        self.color_text()

    def update_syntaxer(self):
        filename = os.path.basename(self.file_path)
        if filename != self.syntaxer.filename:
            self.syntaxer.filename = filename
            syntaxdef = self.app.syntax_factory.get_definition(filename)
            if self.syntaxdef is not syntaxdef:
                self.syntax_needs_color = False
                self.props.syntaxdef = syntaxdef
                self.color_text()
                return
        if self.syntax_needs_color:
            # force re-highlight entire document
            self.syntax_needs_color = False
            self.color_text()

    def on_text_edit(self, rng):
        if self.text_storage.editedMask() == ak.NSTextStorageEditedAttributes:
            # break color text -> attribute change -> color text -> ...
            return
        self.color_text(rng)

    def _coalesce_edit_ranges(self, rng=None):
        rng_ = self._edit_range
        if rng is None or rng_ is ALL:
            rng = None
        elif rng_ is not None:
            rng = union_range(rng, rng_)
        self._edit_range = rng or ALL
        return (self, rng), {}

    @debounce(0.05, _coalesce_edit_ranges)
    def color_text(self, rng):
        if self.text_storage is not None:
            self.syntaxer.color_text(self.text_storage, rng, timeout=0.05)
            self._edit_range = None

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

    @objc.typedSelector(b"v@:@ii")
    def document_shouldClose_contextInfo_(self, doc, should_close, info):
        self.app.context.pop(info)(should_close)

    def close(self):
        self.text_storage = None
        self.props = None
        self.app.document_closed(self)

    # -- TODO refactor NSDocument compat --------------------------------------

    def displayName(self):
        return self.name

    def canCloseDocumentWithDelegate_shouldCloseSelector_contextInfo_(self, delegate, should_close, info):
        raise NotImplementedError