vibefpmgui.source_text
Reading source files: the one set of rules for the editor, Search, Rename and Replace in Files (so they agree on a file's text, its encoding and its line ends). No Qt.
1"""Reading source files: the one set of rules for the editor, Search, 2Rename and Replace in Files (so they agree on a file's text, its encoding 3and its line ends). No Qt. 4""" 5 6from __future__ import annotations 7 8from pathlib import Path 9from typing import NamedTuple 10 11MAX_FILE_SIZE = 8 * 1024 * 1024 # (bigger files aren't searched: see search_text) 12 13 14class SourceText(NamedTuple): 15 text: str # with "\n" line ends 16 encoding: str # "utf-8", or "latin-1" for a file that isn't UTF-8 17 newline: str # the line end most of its lines have: "\n" or "\r\n" 18 mixed: bool # it has more than one kind of line end 19 20 21def line_endings(text: str) -> tuple[str, bool]: 22 """(the line end most lines of a file's text have: "\\n" or "\\r\\n", 23 whether it has more than one kind). Old Mac line ends (a lone "\\r") count 24 as another kind: Qt reads them as line breaks, saved as the common one.""" 25 crlf = text.count("\r\n") 26 lone_cr = text.count("\r") - crlf 27 lf = text.count("\n") - crlf 28 kinds = sum(1 for n in (crlf, lone_cr, lf) if n) 29 return ("\r\n" if crlf > lf else "\n"), kinds > 1 30 31 32def decode(raw: bytes) -> tuple[str, str]: 33 """(text, encoding) of a file's bytes: UTF-8 (with its byte-order mark: 34 "utf-8-sig", the mark not in the text but written back), or Latin-1 35 (which reads any bytes) for one that isn't. (Qt's editor drops a mark 36 left in the text: saved, the file lost it; blamed, its first line was 37 "not committed yet".)""" 38 try: 39 if raw.startswith(b"\xef\xbb\xbf"): 40 return raw.decode("utf-8-sig"), "utf-8-sig" 41 return raw.decode("utf-8"), "utf-8" 42 except UnicodeDecodeError: 43 return raw.decode("latin-1"), "latin-1" 44 45 46def read_source(path: Path) -> SourceText: 47 """A file's text as the editor shows it, and how to write it back (raises 48 OSError if it can't be read).""" 49 text, encoding = decode(path.read_bytes()) 50 newline, mixed = line_endings(text) 51 return SourceText(text.replace("\r\n", "\n"), encoding, newline, mixed) 52 53 54def search_text(path: Path) -> str | None: 55 """A file's text ("\\n" line ends) to search, or None for one that's 56 binary, huge or unreadable.""" 57 try: 58 if path.stat().st_size > MAX_FILE_SIZE: 59 return None 60 raw = path.read_bytes() 61 except OSError: 62 return None 63 if b"\0" in raw[:8192]: 64 return None 65 return decode(raw)[0].replace("\r\n", "\n")
15class SourceText(NamedTuple): 16 text: str # with "\n" line ends 17 encoding: str # "utf-8", or "latin-1" for a file that isn't UTF-8 18 newline: str # the line end most of its lines have: "\n" or "\r\n" 19 mixed: bool # it has more than one kind of line end
SourceText(text, encoding, newline, mixed)
22def line_endings(text: str) -> tuple[str, bool]: 23 """(the line end most lines of a file's text have: "\\n" or "\\r\\n", 24 whether it has more than one kind). Old Mac line ends (a lone "\\r") count 25 as another kind: Qt reads them as line breaks, saved as the common one.""" 26 crlf = text.count("\r\n") 27 lone_cr = text.count("\r") - crlf 28 lf = text.count("\n") - crlf 29 kinds = sum(1 for n in (crlf, lone_cr, lf) if n) 30 return ("\r\n" if crlf > lf else "\n"), kinds > 1
(the line end most lines of a file's text have: "\n" or "\r\n", whether it has more than one kind). Old Mac line ends (a lone "\r") count as another kind: Qt reads them as line breaks, saved as the common one.
33def decode(raw: bytes) -> tuple[str, str]: 34 """(text, encoding) of a file's bytes: UTF-8 (with its byte-order mark: 35 "utf-8-sig", the mark not in the text but written back), or Latin-1 36 (which reads any bytes) for one that isn't. (Qt's editor drops a mark 37 left in the text: saved, the file lost it; blamed, its first line was 38 "not committed yet".)""" 39 try: 40 if raw.startswith(b"\xef\xbb\xbf"): 41 return raw.decode("utf-8-sig"), "utf-8-sig" 42 return raw.decode("utf-8"), "utf-8" 43 except UnicodeDecodeError: 44 return raw.decode("latin-1"), "latin-1"
(text, encoding) of a file's bytes: UTF-8 (with its byte-order mark: "utf-8-sig", the mark not in the text but written back), or Latin-1 (which reads any bytes) for one that isn't. (Qt's editor drops a mark left in the text: saved, the file lost it; blamed, its first line was "not committed yet".)
47def read_source(path: Path) -> SourceText: 48 """A file's text as the editor shows it, and how to write it back (raises 49 OSError if it can't be read).""" 50 text, encoding = decode(path.read_bytes()) 51 newline, mixed = line_endings(text) 52 return SourceText(text.replace("\r\n", "\n"), encoding, newline, mixed)
A file's text as the editor shows it, and how to write it back (raises OSError if it can't be read).
55def search_text(path: Path) -> str | None: 56 """A file's text ("\\n" line ends) to search, or None for one that's 57 binary, huge or unreadable.""" 58 try: 59 if path.stat().st_size > MAX_FILE_SIZE: 60 return None 61 raw = path.read_bytes() 62 except OSError: 63 return None 64 if b"\0" in raw[:8192]: 65 return None 66 return decode(raw)[0].replace("\r\n", "\n")
A file's text ("\n" line ends) to search, or None for one that's binary, huge or unreadable.