diff --git a/mkdocs/livereload/__init__.py b/mkdocs/livereload/__init__.py index 8c4af1c7..8da46dce 100644 --- a/mkdocs/livereload/__init__.py +++ b/mkdocs/livereload/__init__.py @@ -162,6 +162,17 @@ def watch(self, path: str, func: None = None, *, recursive: bool = True) -> None def callback(event): if event.is_directory: return + # Ignore hidden files and editor temporary/backup files: + # - .dotfiles (vim swap .foo.md.swp, .foo.md.swo, .foo.md.swn, etc.) + # - files ending with ~ (editor backup) + # - #files# matching Emacs auto-save pattern + name = os.path.basename(event.src_path) + if ( + name.startswith(".") + or name.endswith("~") + or (name.startswith("#") and name.endswith("#")) + ): + return log.debug(str(event)) with self._rebuild_cond: self._want_rebuild = True diff --git a/mkdocs/tests/livereload_tests.py b/mkdocs/tests/livereload_tests.py index 04594448..4f7d6cdf 100644 --- a/mkdocs/tests/livereload_tests.py +++ b/mkdocs/tests/livereload_tests.py @@ -619,6 +619,46 @@ def test_watches_through_relative_symlinks(self, origin_dir, site_dir): Path(origin_dir, "README.md").write_text("edited") self.assertTrue(started_building.wait(timeout=10)) + @tempdir({"foo.md": "original"}) + def test_ignores_dotfile_changes(self, docs_dir): + """Hidden files (starting with '.') should not trigger rebuild.""" + started_building = threading.Event() + with testing_server(docs_dir, started_building.set) as server: + server.watch(docs_dir) + time.sleep(0.01) + + # Vim swap file + Path(docs_dir, ".foo.md.swp").write_text("swap") + self.assertFalse(started_building.wait(timeout=0.5)) + + # Generic dotfile + Path(docs_dir, ".hidden").write_text("hidden") + self.assertFalse(started_building.wait(timeout=0.5)) + + @tempdir({"foo.md": "original"}) + def test_ignores_tilde_backup_files(self, docs_dir): + """Editor backup files (ending with '~') should not trigger rebuild.""" + started_building = threading.Event() + with testing_server(docs_dir, started_building.set) as server: + server.watch(docs_dir) + time.sleep(0.01) + + # Backup file created by editors + Path(docs_dir, "foo.md~").write_text("backup") + self.assertFalse(started_building.wait(timeout=0.5)) + + @tempdir({"foo.md": "original"}) + def test_ignores_emacs_autosave_files(self, docs_dir): + """Emacs auto-save files (#*#) should not trigger rebuild.""" + started_building = threading.Event() + with testing_server(docs_dir, started_building.set) as server: + server.watch(docs_dir) + time.sleep(0.01) + + # Emacs auto-save pattern + Path(docs_dir, "#foo.md#").write_text("autosave") + self.assertFalse(started_building.wait(timeout=0.5)) + @tempdir() def test_watch_with_broken_symlinks(self, docs_dir): Path(docs_dir, "subdir").mkdir()