document.py 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274
  1. import win32ui
  2. from pywin.mfc import docview
  3. from pywin import default_scintilla_encoding
  4. from . import scintillacon
  5. import win32con
  6. import string
  7. import os
  8. import codecs
  9. import re
  10. crlf_bytes = "\r\n".encode("ascii")
  11. lf_bytes = "\n".encode("ascii")
  12. # re from pep263 - but we use it both on bytes and strings.
  13. re_encoding_bytes = re.compile("coding[:=]\s*([-\w.]+)".encode("ascii"))
  14. re_encoding_text = re.compile("coding[:=]\s*([-\w.]+)")
  15. ParentScintillaDocument=docview.Document
  16. class CScintillaDocument(ParentScintillaDocument):
  17. "A SyntEdit document. "
  18. def __init__(self, *args):
  19. self.bom = None # the BOM, if any, read from the file.
  20. # the encoding we detected from the source. Might have
  21. # detected via the BOM or an encoding decl. Note that in
  22. # the latter case (ie, while self.bom is None), it can't be
  23. # trusted - the user may have edited the encoding decl between
  24. # open and save.
  25. self.source_encoding = None
  26. ParentScintillaDocument.__init__(self, *args)
  27. def DeleteContents(self):
  28. pass
  29. def OnOpenDocument(self, filename):
  30. # init data members
  31. #print "Opening", filename
  32. self.SetPathName(filename) # Must set this early!
  33. try:
  34. # load the text as binary we can get smart
  35. # about detecting any existing EOL conventions.
  36. f = open(filename, 'rb')
  37. try:
  38. self._LoadTextFromFile(f)
  39. finally:
  40. f.close()
  41. except IOError:
  42. rc = win32ui.MessageBox("Could not load the file from %s\n\nDo you want to create a new file?" % filename,
  43. "Pythonwin", win32con.MB_YESNO | win32con.MB_ICONWARNING)
  44. if rc == win32con.IDNO:
  45. return 0
  46. assert rc == win32con.IDYES, rc
  47. try:
  48. f = open(filename, 'wb+')
  49. try:
  50. self._LoadTextFromFile(f)
  51. finally:
  52. f.close()
  53. except IOError as e:
  54. rc = win32ui.MessageBox("Cannot create the file %s" % filename)
  55. return 1
  56. def SaveFile(self, fileName, encoding=None):
  57. view = self.GetFirstView()
  58. ok = view.SaveTextFile(fileName, encoding=encoding)
  59. if ok:
  60. view.SCISetSavePoint()
  61. return ok
  62. def ApplyFormattingStyles(self):
  63. self._ApplyOptionalToViews("ApplyFormattingStyles")
  64. # #####################
  65. # File related functions
  66. # Helper to transfer text from the MFC document to the control.
  67. def _LoadTextFromFile(self, f):
  68. # detect EOL mode - we don't support \r only - so find the
  69. # first '\n' and guess based on the char before.
  70. l = f.readline()
  71. l2 = f.readline()
  72. # If line ends with \r\n or has no line ending, use CRLF.
  73. if l.endswith(crlf_bytes) or not l.endswith(lf_bytes):
  74. eol_mode = scintillacon.SC_EOL_CRLF
  75. else:
  76. eol_mode = scintillacon.SC_EOL_LF
  77. # Detect the encoding - first look for a BOM, and if not found,
  78. # look for a pep263 encoding declaration.
  79. for bom, encoding in (
  80. (codecs.BOM_UTF8, "utf8"),
  81. (codecs.BOM_UTF16_LE, "utf_16_le"),
  82. (codecs.BOM_UTF16_BE, "utf_16_be"),
  83. ):
  84. if l.startswith(bom):
  85. self.bom = bom
  86. self.source_encoding = encoding
  87. l = l[len(bom):] # remove it.
  88. break
  89. else:
  90. # no bom detected - look for pep263 encoding decl.
  91. for look in (l, l2):
  92. # Note we are looking at raw bytes here: so
  93. # both the re itself uses bytes and the result
  94. # is bytes - but we need the result as a string.
  95. match = re_encoding_bytes.search(look)
  96. if match is not None:
  97. self.source_encoding = match.group(1).decode("ascii")
  98. break
  99. # reading by lines would be too slow? Maybe we can use the
  100. # incremental encoders? For now just stick with loading the
  101. # entire file in memory.
  102. text = l + l2 + f.read()
  103. # Translate from source encoding to UTF-8 bytes for Scintilla
  104. source_encoding = self.source_encoding
  105. # If we don't know an encoding, try utf-8 - if that fails we will
  106. # fallback to latin-1 to treat it as bytes...
  107. if source_encoding is None:
  108. source_encoding = 'utf-8'
  109. # we could optimize this by avoiding utf8 to-ing and from-ing,
  110. # but then we would lose the ability to handle invalid utf8
  111. # (and even then, the use of encoding aliases makes this tricky)
  112. # To create an invalid utf8 file:
  113. # >>> open(filename, "wb").write(codecs.BOM_UTF8+"bad \xa9har\r\n")
  114. try:
  115. dec = text.decode(source_encoding)
  116. except UnicodeError:
  117. print("WARNING: Failed to decode bytes from '%s' encoding - treating as latin1" % source_encoding)
  118. dec = text.decode('latin1')
  119. except LookupError:
  120. print("WARNING: Invalid encoding '%s' specified - treating as latin1" % source_encoding)
  121. dec = text.decode('latin1')
  122. # and put it back as utf8 - this shouldn't fail.
  123. text = dec.encode(default_scintilla_encoding)
  124. view = self.GetFirstView()
  125. if view.IsWindow():
  126. # Turn off undo collection while loading
  127. view.SendScintilla(scintillacon.SCI_SETUNDOCOLLECTION, 0, 0)
  128. # Make sure the control isnt read-only
  129. view.SetReadOnly(0)
  130. view.SendScintilla(scintillacon.SCI_CLEARALL)
  131. view.SendMessage(scintillacon.SCI_ADDTEXT, text)
  132. view.SendScintilla(scintillacon.SCI_SETUNDOCOLLECTION, 1, 0)
  133. view.SendScintilla(win32con.EM_EMPTYUNDOBUFFER, 0, 0)
  134. # set EOL mode
  135. view.SendScintilla(scintillacon.SCI_SETEOLMODE, eol_mode)
  136. def _SaveTextToFile(self, view, filename, encoding=None):
  137. s = view.GetTextRange() # already decoded from scintilla's encoding
  138. source_encoding = encoding
  139. if source_encoding is None:
  140. if self.bom:
  141. source_encoding = self.source_encoding
  142. else:
  143. # no BOM - look for an encoding.
  144. bits = re.split("[\r\n]+", s, 3)
  145. for look in bits[:-1]:
  146. match = re_encoding_text.search(look)
  147. if match is not None:
  148. source_encoding = match.group(1)
  149. self.source_encoding = source_encoding
  150. break
  151. if source_encoding is None:
  152. source_encoding = 'utf-8'
  153. ## encode data before opening file so script is not lost if encoding fails
  154. file_contents = s.encode(source_encoding)
  155. # Open in binary mode as scintilla itself ensures the
  156. # line endings are already appropriate
  157. f = open(filename, 'wb')
  158. try:
  159. if self.bom:
  160. f.write(self.bom)
  161. f.write(file_contents)
  162. finally:
  163. f.close()
  164. self.SetModifiedFlag(0)
  165. def FinalizeViewCreation(self, view):
  166. pass
  167. def HookViewNotifications(self, view):
  168. parent = view.GetParentFrame()
  169. parent.HookNotify(ViewNotifyDelegate(self, "OnBraceMatch"), scintillacon.SCN_CHECKBRACE)
  170. parent.HookNotify(ViewNotifyDelegate(self, "OnMarginClick"), scintillacon.SCN_MARGINCLICK)
  171. parent.HookNotify(ViewNotifyDelegate(self, "OnNeedShown"), scintillacon.SCN_NEEDSHOWN)
  172. parent.HookNotify(DocumentNotifyDelegate(self, "OnSavePointReached"), scintillacon.SCN_SAVEPOINTREACHED)
  173. parent.HookNotify(DocumentNotifyDelegate(self, "OnSavePointLeft"), scintillacon.SCN_SAVEPOINTLEFT)
  174. parent.HookNotify(DocumentNotifyDelegate(self, "OnModifyAttemptRO"), scintillacon.SCN_MODIFYATTEMPTRO)
  175. # Tell scintilla what characters should abort auto-complete.
  176. view.SCIAutoCStops(string.whitespace+"()[]:;+-/*=\\?'!#@$%^&,<>\"'|" )
  177. if view != self.GetFirstView():
  178. view.SCISetDocPointer(self.GetFirstView().SCIGetDocPointer())
  179. def OnSavePointReached(self, std, extra):
  180. self.SetModifiedFlag(0)
  181. def OnSavePointLeft(self, std, extra):
  182. self.SetModifiedFlag(1)
  183. def OnModifyAttemptRO(self, std, extra):
  184. self.MakeDocumentWritable()
  185. # All Marker functions are 1 based.
  186. def MarkerAdd( self, lineNo, marker ):
  187. self.GetEditorView().SCIMarkerAdd(lineNo-1, marker)
  188. def MarkerCheck(self, lineNo, marker ):
  189. v = self.GetEditorView()
  190. lineNo = lineNo - 1 # Make 0 based
  191. markerState = v.SCIMarkerGet(lineNo)
  192. return markerState & (1<<marker) != 0
  193. def MarkerToggle( self, lineNo, marker ):
  194. v = self.GetEditorView()
  195. if self.MarkerCheck(lineNo, marker):
  196. v.SCIMarkerDelete(lineNo-1, marker)
  197. else:
  198. v.SCIMarkerAdd(lineNo-1, marker)
  199. def MarkerDelete( self, lineNo, marker ):
  200. self.GetEditorView().SCIMarkerDelete(lineNo-1, marker)
  201. def MarkerDeleteAll( self, marker ):
  202. self.GetEditorView().SCIMarkerDeleteAll(marker)
  203. def MarkerGetNext(self, lineNo, marker):
  204. return self.GetEditorView().SCIMarkerNext( lineNo-1, 1 << marker )+1
  205. def MarkerAtLine(self, lineNo, marker):
  206. markerState = self.GetEditorView().SCIMarkerGet(lineNo-1)
  207. return markerState & (1<<marker)
  208. # Helper for reflecting functions to views.
  209. def _ApplyToViews(self, funcName, *args):
  210. for view in self.GetAllViews():
  211. func = getattr(view, funcName)
  212. func(*args)
  213. def _ApplyOptionalToViews(self, funcName, *args):
  214. for view in self.GetAllViews():
  215. func = getattr(view, funcName, None)
  216. if func is not None:
  217. func(*args)
  218. def GetEditorView(self):
  219. # Find the first frame with a view,
  220. # then ask it to give the editor view
  221. # as it knows which one is "active"
  222. try:
  223. frame_gev = self.GetFirstView().GetParentFrame().GetEditorView
  224. except AttributeError:
  225. return self.GetFirstView()
  226. return frame_gev()
  227. # Delegate to the correct view, based on the control that sent it.
  228. class ViewNotifyDelegate:
  229. def __init__(self, doc, name):
  230. self.doc = doc
  231. self.name = name
  232. def __call__(self, std, extra):
  233. (hwndFrom, idFrom, code) = std
  234. for v in self.doc.GetAllViews():
  235. if v.GetSafeHwnd() == hwndFrom:
  236. return getattr(v, self.name)(*(std, extra))
  237. # Delegate to the document, but only from a single view (as each view sends it seperately)
  238. class DocumentNotifyDelegate:
  239. def __init__(self, doc, name):
  240. self.doc = doc
  241. self.delegate = getattr(doc, name)
  242. def __call__(self, std, extra):
  243. (hwndFrom, idFrom, code) = std
  244. if hwndFrom == self.doc.GetEditorView().GetSafeHwnd():
  245. self.delegate(*(std, extra))