File size: 11,468 Bytes
d12bc25
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
import codecs
import re
import string

import win32con
import win32ui
from pywin import default_scintilla_encoding
from pywin.mfc import docview

from . import scintillacon

crlf_bytes = "\r\n".encode("ascii")
lf_bytes = "\n".encode("ascii")

# re from pep263 - but we use it both on bytes and strings.
re_encoding_bytes = re.compile("coding[:=]\s*([-\w.]+)".encode("ascii"))
re_encoding_text = re.compile("coding[:=]\s*([-\w.]+)")

ParentScintillaDocument = docview.Document


class CScintillaDocument(ParentScintillaDocument):
    "A SyntEdit document."

    def __init__(self, *args):
        self.bom = None  # the BOM, if any, read from the file.
        # the encoding we detected from the source.  Might have
        # detected via the BOM or an encoding decl.  Note that in
        # the latter case (ie, while self.bom is None), it can't be
        # trusted - the user may have edited the encoding decl between
        # open and save.
        self.source_encoding = None
        ParentScintillaDocument.__init__(self, *args)

    def DeleteContents(self):
        pass

    def OnOpenDocument(self, filename):
        # init data members
        # print "Opening", filename
        self.SetPathName(filename)  # Must set this early!
        try:
            # load the text as binary we can get smart
            # about detecting any existing EOL conventions.
            f = open(filename, "rb")
            try:
                self._LoadTextFromFile(f)
            finally:
                f.close()
        except IOError:
            rc = win32ui.MessageBox(
                "Could not load the file from %s\n\nDo you want to create a new file?"
                % filename,
                "Pythonwin",
                win32con.MB_YESNO | win32con.MB_ICONWARNING,
            )
            if rc == win32con.IDNO:
                return 0
            assert rc == win32con.IDYES, rc
            try:
                f = open(filename, "wb+")
                try:
                    self._LoadTextFromFile(f)
                finally:
                    f.close()
            except IOError as e:
                rc = win32ui.MessageBox("Cannot create the file %s" % filename)
        return 1

    def SaveFile(self, fileName, encoding=None):
        view = self.GetFirstView()
        ok = view.SaveTextFile(fileName, encoding=encoding)
        if ok:
            view.SCISetSavePoint()
        return ok

    def ApplyFormattingStyles(self):
        self._ApplyOptionalToViews("ApplyFormattingStyles")

    # #####################
    # File related functions
    # Helper to transfer text from the MFC document to the control.
    def _LoadTextFromFile(self, f):
        # detect EOL mode - we don't support \r only - so find the
        # first '\n' and guess based on the char before.
        l = f.readline()
        l2 = f.readline()
        # If line ends with \r\n or has no line ending, use CRLF.
        if l.endswith(crlf_bytes) or not l.endswith(lf_bytes):
            eol_mode = scintillacon.SC_EOL_CRLF
        else:
            eol_mode = scintillacon.SC_EOL_LF

        # Detect the encoding - first look for a BOM, and if not found,
        # look for a pep263 encoding declaration.
        for bom, encoding in (
            (codecs.BOM_UTF8, "utf8"),
            (codecs.BOM_UTF16_LE, "utf_16_le"),
            (codecs.BOM_UTF16_BE, "utf_16_be"),
        ):
            if l.startswith(bom):
                self.bom = bom
                self.source_encoding = encoding
                l = l[len(bom) :]  # remove it.
                break
        else:
            # no bom detected - look for pep263 encoding decl.
            for look in (l, l2):
                # Note we are looking at raw bytes here: so
                # both the re itself uses bytes and the result
                # is bytes - but we need the result as a string.
                match = re_encoding_bytes.search(look)
                if match is not None:
                    self.source_encoding = match.group(1).decode("ascii")
                    break

        # reading by lines would be too slow?  Maybe we can use the
        # incremental encoders? For now just stick with loading the
        # entire file in memory.
        text = l + l2 + f.read()

        # Translate from source encoding to UTF-8 bytes for Scintilla
        source_encoding = self.source_encoding
        # If we don't know an encoding, try utf-8 - if that fails we will
        # fallback to latin-1 to treat it as bytes...
        if source_encoding is None:
            source_encoding = "utf-8"
        # we could optimize this by avoiding utf8 to-ing and from-ing,
        # but then we would lose the ability to handle invalid utf8
        # (and even then, the use of encoding aliases makes this tricky)
        # To create an invalid utf8 file:
        # >>> open(filename, "wb").write(codecs.BOM_UTF8+"bad \xa9har\r\n")
        try:
            dec = text.decode(source_encoding)
        except UnicodeError:
            print(
                "WARNING: Failed to decode bytes from '%s' encoding - treating as latin1"
                % source_encoding
            )
            dec = text.decode("latin1")
        except LookupError:
            print(
                "WARNING: Invalid encoding '%s' specified - treating as latin1"
                % source_encoding
            )
            dec = text.decode("latin1")
        # and put it back as utf8 - this shouldn't fail.
        text = dec.encode(default_scintilla_encoding)

        view = self.GetFirstView()
        if view.IsWindow():
            # Turn off undo collection while loading
            view.SendScintilla(scintillacon.SCI_SETUNDOCOLLECTION, 0, 0)
            # Make sure the control isnt read-only
            view.SetReadOnly(0)
            view.SendScintilla(scintillacon.SCI_CLEARALL)
            view.SendMessage(scintillacon.SCI_ADDTEXT, text)
            view.SendScintilla(scintillacon.SCI_SETUNDOCOLLECTION, 1, 0)
            view.SendScintilla(win32con.EM_EMPTYUNDOBUFFER, 0, 0)
            # set EOL mode
            view.SendScintilla(scintillacon.SCI_SETEOLMODE, eol_mode)

    def _SaveTextToFile(self, view, filename, encoding=None):
        s = view.GetTextRange()  # already decoded from scintilla's encoding
        source_encoding = encoding
        if source_encoding is None:
            if self.bom:
                source_encoding = self.source_encoding
            else:
                # no BOM - look for an encoding.
                bits = re.split("[\r\n]+", s, 3)
                for look in bits[:-1]:
                    match = re_encoding_text.search(look)
                    if match is not None:
                        source_encoding = match.group(1)
                        self.source_encoding = source_encoding
                        break

            if source_encoding is None:
                source_encoding = "utf-8"

        ## encode data before opening file so script is not lost if encoding fails
        file_contents = s.encode(source_encoding)
        # Open in binary mode as scintilla itself ensures the
        # line endings are already appropriate
        f = open(filename, "wb")
        try:
            if self.bom:
                f.write(self.bom)
            f.write(file_contents)
        finally:
            f.close()
        self.SetModifiedFlag(0)

    def FinalizeViewCreation(self, view):
        pass

    def HookViewNotifications(self, view):
        parent = view.GetParentFrame()
        parent.HookNotify(
            ViewNotifyDelegate(self, "OnBraceMatch"), scintillacon.SCN_CHECKBRACE
        )
        parent.HookNotify(
            ViewNotifyDelegate(self, "OnMarginClick"), scintillacon.SCN_MARGINCLICK
        )
        parent.HookNotify(
            ViewNotifyDelegate(self, "OnNeedShown"), scintillacon.SCN_NEEDSHOWN
        )

        parent.HookNotify(
            DocumentNotifyDelegate(self, "OnSavePointReached"),
            scintillacon.SCN_SAVEPOINTREACHED,
        )
        parent.HookNotify(
            DocumentNotifyDelegate(self, "OnSavePointLeft"),
            scintillacon.SCN_SAVEPOINTLEFT,
        )
        parent.HookNotify(
            DocumentNotifyDelegate(self, "OnModifyAttemptRO"),
            scintillacon.SCN_MODIFYATTEMPTRO,
        )
        # Tell scintilla what characters should abort auto-complete.
        view.SCIAutoCStops(string.whitespace + "()[]:;+-/*=\\?'!#@$%^&,<>\"'|")

        if view != self.GetFirstView():
            view.SCISetDocPointer(self.GetFirstView().SCIGetDocPointer())

    def OnSavePointReached(self, std, extra):
        self.SetModifiedFlag(0)

    def OnSavePointLeft(self, std, extra):
        self.SetModifiedFlag(1)

    def OnModifyAttemptRO(self, std, extra):
        self.MakeDocumentWritable()

    # All Marker functions are 1 based.
    def MarkerAdd(self, lineNo, marker):
        self.GetEditorView().SCIMarkerAdd(lineNo - 1, marker)

    def MarkerCheck(self, lineNo, marker):
        v = self.GetEditorView()
        lineNo = lineNo - 1  # Make 0 based
        markerState = v.SCIMarkerGet(lineNo)
        return markerState & (1 << marker) != 0

    def MarkerToggle(self, lineNo, marker):
        v = self.GetEditorView()
        if self.MarkerCheck(lineNo, marker):
            v.SCIMarkerDelete(lineNo - 1, marker)
        else:
            v.SCIMarkerAdd(lineNo - 1, marker)

    def MarkerDelete(self, lineNo, marker):
        self.GetEditorView().SCIMarkerDelete(lineNo - 1, marker)

    def MarkerDeleteAll(self, marker):
        self.GetEditorView().SCIMarkerDeleteAll(marker)

    def MarkerGetNext(self, lineNo, marker):
        return self.GetEditorView().SCIMarkerNext(lineNo - 1, 1 << marker) + 1

    def MarkerAtLine(self, lineNo, marker):
        markerState = self.GetEditorView().SCIMarkerGet(lineNo - 1)
        return markerState & (1 << marker)

    # Helper for reflecting functions to views.
    def _ApplyToViews(self, funcName, *args):
        for view in self.GetAllViews():
            func = getattr(view, funcName)
            func(*args)

    def _ApplyOptionalToViews(self, funcName, *args):
        for view in self.GetAllViews():
            func = getattr(view, funcName, None)
            if func is not None:
                func(*args)

    def GetEditorView(self):
        # Find the first frame with a view,
        # then ask it to give the editor view
        # as it knows which one is "active"
        try:
            frame_gev = self.GetFirstView().GetParentFrame().GetEditorView
        except AttributeError:
            return self.GetFirstView()
        return frame_gev()


# Delegate to the correct view, based on the control that sent it.
class ViewNotifyDelegate:
    def __init__(self, doc, name):
        self.doc = doc
        self.name = name

    def __call__(self, std, extra):
        (hwndFrom, idFrom, code) = std
        for v in self.doc.GetAllViews():
            if v.GetSafeHwnd() == hwndFrom:
                return getattr(v, self.name)(*(std, extra))


# Delegate to the document, but only from a single view (as each view sends it seperately)
class DocumentNotifyDelegate:
    def __init__(self, doc, name):
        self.doc = doc
        self.delegate = getattr(doc, name)

    def __call__(self, std, extra):
        (hwndFrom, idFrom, code) = std
        if hwndFrom == self.doc.GetEditorView().GetSafeHwnd():
            self.delegate(*(std, extra))