filesystem.py 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118
  1. from __future__ import annotations
  2. import os
  3. from typing import Callable, Iterable
  4. from prompt_toolkit.completion import CompleteEvent, Completer, Completion
  5. from prompt_toolkit.document import Document
  6. __all__ = [
  7. "PathCompleter",
  8. "ExecutableCompleter",
  9. ]
  10. class PathCompleter(Completer):
  11. """
  12. Complete for Path variables.
  13. :param get_paths: Callable which returns a list of directories to look into
  14. when the user enters a relative path.
  15. :param file_filter: Callable which takes a filename and returns whether
  16. this file should show up in the completion. ``None``
  17. when no filtering has to be done.
  18. :param min_input_len: Don't do autocompletion when the input string is shorter.
  19. """
  20. def __init__(
  21. self,
  22. only_directories: bool = False,
  23. get_paths: Callable[[], list[str]] | None = None,
  24. file_filter: Callable[[str], bool] | None = None,
  25. min_input_len: int = 0,
  26. expanduser: bool = False,
  27. ) -> None:
  28. self.only_directories = only_directories
  29. self.get_paths = get_paths or (lambda: ["."])
  30. self.file_filter = file_filter or (lambda _: True)
  31. self.min_input_len = min_input_len
  32. self.expanduser = expanduser
  33. def get_completions(
  34. self, document: Document, complete_event: CompleteEvent
  35. ) -> Iterable[Completion]:
  36. text = document.text_before_cursor
  37. # Complete only when we have at least the minimal input length,
  38. # otherwise, we can too many results and autocompletion will become too
  39. # heavy.
  40. if len(text) < self.min_input_len:
  41. return
  42. try:
  43. # Do tilde expansion.
  44. if self.expanduser:
  45. text = os.path.expanduser(text)
  46. # Directories where to look.
  47. dirname = os.path.dirname(text)
  48. if dirname:
  49. directories = [
  50. os.path.dirname(os.path.join(p, text)) for p in self.get_paths()
  51. ]
  52. else:
  53. directories = self.get_paths()
  54. # Start of current file.
  55. prefix = os.path.basename(text)
  56. # Get all filenames.
  57. filenames = []
  58. for directory in directories:
  59. # Look for matches in this directory.
  60. if os.path.isdir(directory):
  61. for filename in os.listdir(directory):
  62. if filename.startswith(prefix):
  63. filenames.append((directory, filename))
  64. # Sort
  65. filenames = sorted(filenames, key=lambda k: k[1])
  66. # Yield them.
  67. for directory, filename in filenames:
  68. completion = filename[len(prefix) :]
  69. full_name = os.path.join(directory, filename)
  70. if os.path.isdir(full_name):
  71. # For directories, add a slash to the filename.
  72. # (We don't add them to the `completion`. Users can type it
  73. # to trigger the autocompletion themselves.)
  74. filename += "/"
  75. elif self.only_directories:
  76. continue
  77. if not self.file_filter(full_name):
  78. continue
  79. yield Completion(
  80. text=completion,
  81. start_position=0,
  82. display=filename,
  83. )
  84. except OSError:
  85. pass
  86. class ExecutableCompleter(PathCompleter):
  87. """
  88. Complete only executable files in the current path.
  89. """
  90. def __init__(self) -> None:
  91. super().__init__(
  92. only_directories=False,
  93. min_input_len=1,
  94. get_paths=lambda: os.environ.get("PATH", "").split(os.pathsep),
  95. file_filter=lambda name: os.access(name, os.X_OK),
  96. expanduser=True,
  97. )