430 lines
15 KiB
Python
430 lines
15 KiB
Python
"""
|
|
This module contains the file helper implementation
|
|
|
|
"""
|
|
try:
|
|
from future.builtins import open
|
|
from future.builtins import str
|
|
except:
|
|
pass # python 3.2 not supported
|
|
import locale
|
|
import logging
|
|
import mimetypes
|
|
import os
|
|
from pyqode.core.api.manager import Manager
|
|
from pyqode.core.api.utils import TextHelper
|
|
from pyqode.qt import QtCore, QtWidgets
|
|
from pyqode.core.cache import Cache
|
|
|
|
|
|
# needed on windows
|
|
mimetypes.add_type('text/x-python', '.py')
|
|
mimetypes.add_type('text/xml', '.ui')
|
|
|
|
|
|
def _logger():
|
|
return logging.getLogger(__name__)
|
|
|
|
|
|
class FileManager(Manager):
|
|
"""
|
|
Helps manage file operations:
|
|
- opening and saving files
|
|
- providing file icon
|
|
- detecting mimetype
|
|
|
|
Example of usage::
|
|
|
|
editor = CodeEdit()
|
|
assert editor.file.path == ''
|
|
# open a file with default locale encoding or using the cached one.
|
|
editor.open(__file__)
|
|
assert editor.file.path == __file__
|
|
print(editor.file.encoding)
|
|
|
|
# reload with another encoding
|
|
editor.open(__file__, encoding='cp1252', use_cached_encoding=False)
|
|
assert editor.file.path == __file__
|
|
editor.file.encoding == 'cp1252'
|
|
|
|
"""
|
|
class EOL:
|
|
"""
|
|
This class enumerates the possible EOL conventions:
|
|
- System: apply the system EOL
|
|
- Linux: force the use of Linux EOL (\n)
|
|
- Mac: force the use of Macintosh EOL (\r)
|
|
- Windows: force the use of Windows EOL (\r\n)
|
|
"""
|
|
#:
|
|
System = 0
|
|
#: Linux EOL: \n
|
|
Linux = 1
|
|
#: Macintosh EOL: \r
|
|
Mac = 2
|
|
#: Windows EOL: \r\n
|
|
Windows = 3
|
|
|
|
_map = {
|
|
System: os.linesep,
|
|
Linux: '\n',
|
|
Mac: '\r',
|
|
Windows: '\r\n'
|
|
}
|
|
|
|
@classmethod
|
|
def string(cls, value):
|
|
return cls._map[value]
|
|
|
|
@property
|
|
def path(self):
|
|
""" Gets the file path """
|
|
if self._path:
|
|
return os.path.normpath(self._path)
|
|
return ''
|
|
|
|
@property
|
|
def name(self):
|
|
""" Gets the file base name """
|
|
return os.path.split(self.path)[1]
|
|
|
|
@property
|
|
def extension(self):
|
|
""" Gets the file path """
|
|
return os.path.splitext(self.path)[1]
|
|
|
|
@property
|
|
def dirname(self):
|
|
""" Gets the file directory name """
|
|
return os.path.dirname(self._path)
|
|
|
|
@property
|
|
def encoding(self):
|
|
""" Gets the file encoding """
|
|
return self._encoding
|
|
|
|
@property
|
|
def icon(self):
|
|
""" Gets the file icon, provided by _get_icon """
|
|
return self._get_icon()
|
|
|
|
@property
|
|
def autodetect_eol(self):
|
|
return self._autodetect_eol
|
|
|
|
@autodetect_eol.setter
|
|
def autodetect_eol(self, value):
|
|
self._autodetect_eol = value
|
|
if not self._autodetect_eol:
|
|
self._eol = self.EOL.string(self._preferred_eol)
|
|
|
|
@property
|
|
def preferred_eol(self):
|
|
return self._preferred_eol
|
|
|
|
@preferred_eol.setter
|
|
def preferred_eol(self, eol):
|
|
self._preferred_eol = eol
|
|
if not self._autodetect_eol:
|
|
self._eol = self.EOL.string(self._preferred_eol)
|
|
|
|
@property
|
|
def file_size_limit(self):
|
|
"""
|
|
Returns the file size limit. If the size of the file to open
|
|
is superior to the limit, then we disabled syntax highlighting, code
|
|
folding,... to improve the load time and the runtime performances.
|
|
|
|
Default is 10MB.
|
|
"""
|
|
return self._limit
|
|
|
|
@file_size_limit.setter
|
|
def file_size_limit(self, value):
|
|
self._limit = value
|
|
|
|
def _get_icon(self):
|
|
return QtWidgets.QFileIconProvider().icon(QtCore.QFileInfo(self.path))
|
|
|
|
def __init__(self, editor, replace_tabs_by_spaces=True):
|
|
"""
|
|
:param editor: Code edit instance to work on.
|
|
:param replace_tabs_by_spaces: True to replace tabs by spaces on
|
|
load/save.
|
|
"""
|
|
super(FileManager, self).__init__(editor)
|
|
self._limit = 10000000
|
|
self._path = ''
|
|
#: File mimetype
|
|
self.mimetype = ''
|
|
#: store the last file encoding used to open or save the file.
|
|
self._encoding = locale.getpreferredencoding()
|
|
#: True to replace tabs by spaces
|
|
self.replace_tabs_by_spaces = replace_tabs_by_spaces
|
|
#: Opening flag. Set to true during the opening of a file.
|
|
self.opening = False
|
|
#: Saving flag. Set to while saving the editor content to a file.
|
|
self.saving = True
|
|
#: If True, the file is saved to a temporary file first. If the save
|
|
#: went fine, the temporary file is renamed to the final filename.
|
|
self.safe_save = True
|
|
#: True to clean trailing whitespaces of changed lines. Default is
|
|
#: True
|
|
self.clean_trailing_whitespaces = True
|
|
#: True to restore cursor position (if the document has already been
|
|
# opened once).
|
|
self.restore_cursor = True
|
|
#: Preferred EOL convention. This setting will be used for saving the
|
|
#: document unles autodetect_eol is True.
|
|
self._preferred_eol = self.EOL.System
|
|
self._eol = self.EOL.string(self._preferred_eol)
|
|
#: If true, automatically detects file EOL and use it instead of the
|
|
#: preferred EOL when saving files.
|
|
self._autodetect_eol = True
|
|
|
|
@staticmethod
|
|
def get_mimetype(path):
|
|
"""
|
|
Guesses the mime type of a file. If mime type cannot be detected, plain
|
|
text is assumed.
|
|
|
|
:param path: path of the file
|
|
:return: the corresponding mime type.
|
|
"""
|
|
filename = os.path.split(path)[1]
|
|
_logger().debug('detecting mimetype for %s', filename)
|
|
mimetype = mimetypes.guess_type(filename)[0]
|
|
if mimetype is None:
|
|
mimetype = 'text/x-plain'
|
|
_logger().debug('mimetype detected: %s', mimetype)
|
|
return mimetype
|
|
|
|
def open(self, path, encoding=None, use_cached_encoding=True):
|
|
"""
|
|
Open a file and set its content on the editor widget.
|
|
|
|
pyqode does not try to guess encoding. It's up to the client code to
|
|
handle encodings. You can either use a charset detector to detect
|
|
encoding or rely on a settings in your application. It is also up to
|
|
you to handle UnicodeDecodeError, unless you've added
|
|
class:`pyqode.core.panels.EncodingPanel` on the editor.
|
|
|
|
pyqode automatically caches file encoding that you can later reuse it
|
|
automatically.
|
|
|
|
:param path: Path of the file to open.
|
|
:param encoding: Default file encoding. Default is to use the locale
|
|
encoding.
|
|
:param use_cached_encoding: True to use the cached encoding instead
|
|
of ``encoding``. Set it to True if you want to force reload with a
|
|
new encoding.
|
|
|
|
:raises: UnicodeDecodeError in case of error if no EncodingPanel
|
|
were set on the editor.
|
|
"""
|
|
ret_val = False
|
|
if encoding is None:
|
|
encoding = locale.getpreferredencoding()
|
|
self.opening = True
|
|
settings = Cache()
|
|
self._path = path
|
|
# get encoding from cache
|
|
if use_cached_encoding:
|
|
try:
|
|
cached_encoding = settings.get_file_encoding(path)
|
|
except KeyError:
|
|
pass
|
|
else:
|
|
encoding = cached_encoding
|
|
enable_modes = os.path.getsize(path) < self._limit
|
|
for m in self.editor.modes:
|
|
m.enabled = enable_modes
|
|
if not enable_modes:
|
|
self.editor.modes.clear()
|
|
# open file and get its content
|
|
try:
|
|
with open(path, 'Ur', encoding=encoding) as file:
|
|
content = file.read()
|
|
if self.autodetect_eol:
|
|
self._eol = file.newlines
|
|
if isinstance(self._eol, tuple):
|
|
self._eol = self._eol[0]
|
|
if self._eol is None:
|
|
# empty file has no newlines
|
|
self._eol = self.EOL.string(self.preferred_eol)
|
|
else:
|
|
self._eol = self.EOL.string(self.preferred_eol)
|
|
except (UnicodeDecodeError, UnicodeError) as e:
|
|
try:
|
|
from pyqode.core.panels import EncodingPanel
|
|
panel = self.editor.panels.get(EncodingPanel)
|
|
except KeyError:
|
|
raise e # panel not found, not automatic error management
|
|
else:
|
|
panel.on_open_failed(path, encoding)
|
|
else:
|
|
# success! Cache the encoding
|
|
settings.set_file_encoding(path, encoding)
|
|
self._encoding = encoding
|
|
# replace tabs by spaces
|
|
if self.replace_tabs_by_spaces:
|
|
content = content.replace("\t", " " * self.editor.tab_length)
|
|
# set plain text
|
|
self.editor.setPlainText(
|
|
content, self.get_mimetype(path), self.encoding)
|
|
self.editor.setDocumentTitle(self.editor.file.name)
|
|
ret_val = True
|
|
_logger().debug('file open: %s', path)
|
|
self.opening = False
|
|
if self.restore_cursor:
|
|
self._restore_cached_pos()
|
|
return ret_val
|
|
|
|
def _restore_cached_pos(self):
|
|
pos = Cache().get_cursor_position(self.path)
|
|
tc = self.editor.textCursor()
|
|
tc.setPosition(pos)
|
|
self.editor.setTextCursor(tc)
|
|
QtCore.QTimer.singleShot(1, self.editor.centerCursor)
|
|
|
|
def reload(self, encoding):
|
|
"""
|
|
Reload the file with another encoding.
|
|
|
|
:param encoding: the new encoding to use to reload the file.
|
|
"""
|
|
assert os.path.exists(self.path)
|
|
self.open(self.path, encoding=encoding,
|
|
use_cached_encoding=False)
|
|
|
|
@staticmethod
|
|
def _rm(tmp_path):
|
|
try:
|
|
os.remove(tmp_path)
|
|
except OSError:
|
|
pass
|
|
|
|
def _reset_selection(self, sel_end, sel_start):
|
|
text_cursor = self.editor.textCursor()
|
|
text_cursor.setPosition(sel_start)
|
|
text_cursor.setPosition(sel_end, text_cursor.KeepAnchor)
|
|
self.editor.setTextCursor(text_cursor)
|
|
|
|
def _get_selection(self):
|
|
sel_start = self.editor.textCursor().selectionStart()
|
|
sel_end = self.editor.textCursor().selectionEnd()
|
|
return sel_end, sel_start
|
|
|
|
def _get_text(self, encoding):
|
|
lines = self.editor.toPlainText().splitlines()
|
|
if self.clean_trailing_whitespaces:
|
|
lines = [l.rstrip() for l in lines]
|
|
# remove emtpy ending lines
|
|
try:
|
|
last_line = lines[-1]
|
|
except IndexError:
|
|
pass # empty file
|
|
else:
|
|
while last_line == '':
|
|
try:
|
|
lines.pop()
|
|
last_line = lines[-1]
|
|
except IndexError:
|
|
last_line = None
|
|
text = self._eol.join(lines) + self._eol
|
|
return text.encode(encoding)
|
|
|
|
def save(self, path=None, encoding=None, fallback_encoding=None):
|
|
"""
|
|
Save the editor content to a file.
|
|
|
|
:param path: optional file path. Set it to None to save using the
|
|
current path (save), set a new path to save as.
|
|
:param encoding: optional encoding, will use the current
|
|
file encoding if None.
|
|
:param fallback_encoding: Fallback encoding to use in case of encoding
|
|
error. None to use the locale preferred encoding
|
|
|
|
"""
|
|
if not self.editor.dirty and \
|
|
(encoding is None or encoding == self.encoding) and \
|
|
(path is None or path == self.path):
|
|
return
|
|
if fallback_encoding is None:
|
|
fallback_encoding = locale.getpreferredencoding()
|
|
_logger().debug(
|
|
"saving %r to %r with %r encoding", self.path, path, encoding)
|
|
if path is None:
|
|
if self.path:
|
|
path = self.path
|
|
else:
|
|
_logger().debug(
|
|
'failed to save file, path argument cannot be None if '
|
|
'FileManager.path is also None')
|
|
return False
|
|
# use cached encoding if None were specified
|
|
if encoding is None:
|
|
encoding = self._encoding
|
|
self.saving = True
|
|
self.editor.text_saving.emit(str(path))
|
|
# perform a safe save: we first save to a temporary file, if the save
|
|
# succeeded we just rename the temporary file to the final file name
|
|
# and remove it.
|
|
if self.safe_save:
|
|
tmp_path = path + '~'
|
|
else:
|
|
tmp_path = path
|
|
try:
|
|
_logger().debug('saving editor content to temp file: %s', path)
|
|
with open(tmp_path, 'wb') as file:
|
|
file.write(self._get_text(encoding))
|
|
except UnicodeEncodeError:
|
|
# fallback to utf-8 in case of error.
|
|
with open(tmp_path, 'wb') as file:
|
|
file.write(self._get_text(fallback_encoding))
|
|
except (IOError, OSError) as e:
|
|
self._rm(tmp_path)
|
|
self.saving = False
|
|
self.editor.text_saved.emit(str(path))
|
|
raise e
|
|
else:
|
|
# cache update encoding
|
|
Cache().set_file_encoding(path, encoding)
|
|
self._encoding = encoding
|
|
# remove path and rename temp file, if safe save is on
|
|
if self.safe_save:
|
|
_logger().debug('rename %s to %s', tmp_path, path)
|
|
self._rm(path)
|
|
os.rename(tmp_path, path)
|
|
self._rm(tmp_path)
|
|
# reset dirty flags
|
|
self.editor.document().setModified(False)
|
|
# remember path for next save
|
|
self._path = os.path.normpath(path)
|
|
self.editor.text_saved.emit(str(path))
|
|
self.saving = False
|
|
_logger().debug('file saved: %s', path)
|
|
|
|
def close(self, clear=True):
|
|
"""
|
|
Close the file open in the editor:
|
|
- clear editor content
|
|
- reset file attributes to their default values
|
|
|
|
:param clear: True to clear the editor content. Default is True.
|
|
"""
|
|
Cache().set_cursor_position(
|
|
self.path, self.editor.textCursor().position())
|
|
self.editor._original_text = ''
|
|
if clear:
|
|
self.editor.clear()
|
|
self._path = ''
|
|
self.mimetype = ''
|
|
self._encoding = locale.getpreferredencoding()
|
|
|
|
def clone_settings(self, original):
|
|
self.replace_tabs_by_spaces = original.replace_tabs_by_spaces
|
|
self.safe_save = original.replace_tabs_by_spaces
|
|
self.clean_trailing_whitespaces = original.clean_trailing_whitespaces
|
|
self.restore_cursor = original.restore_cursor
|