laburnum 1.17.1

An LSP framework for building language servers and compilers, powered by an incremental query tree with content-addressed storage, task-based dataflow, and parallel queries.
Documentation
// Copyright Two Neutron Stars Incorporated and contributors
// SPDX-License-Identifier: BlueOak-1.0.0

use {
  super::*,
  crate::{
    fs::errors::{
      FsError,
      IoErrorExt,
      Result,
    },
    uri::Uri,
  },
  error_stack::Report,
  ignore::WalkBuilder,
  std::{
    fs::{
      self,
      File,
      OpenOptions as FsOpenOptions,
    },
    io::{
      Read,
      Write,
    },
    path::{
      Path,
      PathBuf,
    },
    time::SystemTime,
  },
};

#[derive(Clone)]
pub struct PhysicalFileSystem {
  root: crate::Uri,
}

#[derive(Clone)]
struct PhysicalDirEntry {
  path:     crate::Uri,
  metadata: std::fs::Metadata,
}

impl std::fmt::Debug for PhysicalDirEntry {
  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    write!(f, "PhysicalDirEntry({})", self.path)
  }
}

impl std::hash::Hash for PhysicalDirEntry {
  fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
    self.path.hash(state);
  }
}

struct PhysicalFileHandle {
  file: File,
}

impl PhysicalFileSystem {
  #[allow(clippy::new_ret_no_self)]
  pub fn new(root: crate::Uri) -> Result<FS> {
    if root.scheme().as_str() != "file" {
      return Err(
        Report::new(FsError::invalid_operation(
          "PhysicalFileSystem only supports file:// URIs",
        ))
        .attach_printable(format!("URI: {root}")),
      );
    }
    Ok(FS::from(Self { root }))
  }

  pub fn new_from_string(root: &str) -> Result<FS> {
    Ok(FS::from(Self {
      root: Self::path_to_uri(Path::new(root))?,
    }))
  }

  pub fn new_with_path(root: &std::path::Path) -> Result<FS> {
    Ok(FS::from(Self {
      root: Self::path_to_uri(root)?,
    }))
  }

  fn uri_to_path(&self, uri: &crate::Uri) -> Result<PathBuf> {
    if uri.scheme().as_str() != "file" {
      return Err(
        Report::new(FsError::invalid_operation(
          "PhysicalFileSystem only supports file:// URIs",
        ))
        .attach_printable(format!("URI: {uri}")),
      );
    }
    uri
      .to_file_path()
      .map(|cow| cow.into_owned())
      .ok_or_else(|| {
        Report::new(FsError::path_conversion_error("Invalid file URI"))
          .attach_printable(format!("URI: {uri}"))
      })
  }

  fn path_to_uri(path: &Path) -> Result<crate::Uri> {
    Uri::from_file_path(path).ok_or_else(|| {
      Report::new(FsError::path_conversion_error("Invalid file path"))
        .attach_printable(format!("Path: {}", path.display()))
    })
  }

  fn convert_metadata(metadata: std::fs::Metadata) -> Metadata {
    Metadata {
      is_dir:   metadata.is_dir(),
      len:      metadata.len(),
      modified: metadata.modified().unwrap_or(SystemTime::now()),
      created:  metadata.created().unwrap_or(SystemTime::now()),
      readonly: metadata.permissions().readonly(),
    }
  }
}

impl FileSystem for PhysicalFileSystem {
  fn uri(&self) -> crate::Uri {
    self.root.clone()
  }

  fn open(
    &self,
    path: &crate::Uri,
    options: OpenOptions,
  ) -> Result<Box<dyn FileHandle>> {
    let path_buf = self.uri_to_path(path)?;
    let file = FsOpenOptions::new()
      .read(options.read)
      .write(options.write)
      .create(options.create)
      .append(options.append)
      .truncate(options.truncate)
      .open(&path_buf)
      .map_err(|e| e.to_fs_error_with_path(&path_buf))?;

    Ok(Box::new(PhysicalFileHandle { file }))
  }

  fn read(&self, path: &crate::Uri) -> Result<Vec<u8>> {
    let path_buf = self.uri_to_path(path)?;
    fs::read(&path_buf).map_err(|e| e.to_fs_error_with_path(&path_buf))
  }

  fn read_to_new_rope(&self, path: &crate::Uri) -> Result<ropey::Rope> {
    let path_buf = self.uri_to_path(path)?;
    let file =
      File::open(&path_buf).map_err(|e| e.to_fs_error_with_path(&path_buf))?;
    ropey::Rope::from_reader(file).map_err(|e| {
      Report::new(FsError::io_error(format!("Failed to create rope: {e}")))
        .attach_printable(format!("Path: {}", path_buf.display()))
    })
  }

  fn write(&self, path: &crate::Uri, data: &[u8]) -> Result<()> {
    let path_buf = self.uri_to_path(path)?;
    if let Some(parent) = path_buf.parent() {
      fs::create_dir_all(parent)
        .map_err(|e| e.to_fs_error_with_path(parent))?;
    }
    fs::write(&path_buf, data).map_err(|e| {
      e.to_fs_error_with_path(&path_buf)
        .attach_printable(format!("Failed to write {} bytes", data.len()))
    })
  }

  fn write_str(&self, path: &crate::Uri, data: &str) -> Result<()> {
    self.write(path, data.as_bytes())
  }

  fn append(&self, path: &crate::Uri, data: &[u8]) -> Result<()> {
    let path_buf = self.uri_to_path(path)?;
    if let Some(parent) = path_buf.parent() {
      fs::create_dir_all(parent)
        .map_err(|e| e.to_fs_error_with_path(parent))?;
    }
    let mut file = FsOpenOptions::new()
      .append(true)
      .create(true)
      .open(&path_buf)
      .map_err(|e| e.to_fs_error_with_path(&path_buf))?;

    file.write_all(data).map_err(|e| {
      e.to_fs_error_with_path(&path_buf)
        .attach_printable(format!("Failed to append {} bytes", data.len()))
    })
  }

  fn delete(&self, path: &crate::Uri) -> Result<()> {
    let path_buf = self.uri_to_path(path)?;
    if path_buf.is_dir() {
      fs::remove_dir_all(&path_buf).map_err(|e| {
        e.to_fs_error_with_path(&path_buf)
          .attach_printable("Failed to remove directory recursively")
      })
    } else {
      fs::remove_file(&path_buf).map_err(|e| {
        e.to_fs_error_with_path(&path_buf)
          .attach_printable("Failed to remove file")
      })
    }
  }

  fn metadata(&self, path: &crate::Uri) -> Result<Metadata> {
    let path_buf = self.uri_to_path(path)?;
    let metadata = fs::metadata(&path_buf).map_err(|e| {
      e.to_fs_error_with_path(&path_buf)
        .attach_printable("Failed to get file metadata")
    })?;
    Ok(Self::convert_metadata(metadata))
  }

  fn root(&self) -> Result<Box<dyn DirEntry>> {
    let path = self.uri_to_path(&self.root)?;
    let metadata = fs::metadata(&path).map_err(|e| {
      e.to_fs_error_with_path(&path)
        .attach_printable("Failed to get root metadata")
    })?;
    Ok(Box::new(PhysicalDirEntry {
      path: self.root.clone(),
      metadata,
    }))
  }

  fn dir(&self, path: &crate::Uri) -> Result<Box<dyn DirEntry>> {
    let path_buf = self.uri_to_path(path)?;
    fs::create_dir_all(&path_buf).map_err(|e| {
      e.to_fs_error_with_path(&path_buf)
        .attach_printable("Failed to create directory")
    })?;

    let metadata = fs::metadata(&path_buf).map_err(|e| {
      e.to_fs_error_with_path(&path_buf)
        .attach_printable("Failed to get directory metadata")
    })?;

    Ok(Box::new(PhysicalDirEntry {
      path: path.clone(),
      metadata,
    }))
  }

  fn find(
    &self,
    path: &crate::Uri,
    globs: &[String],
  ) -> Result<im::Vector<Arc<dyn DirEntry>>> {
    let glob = crate::fs::filesystem::build_glob_set(globs)?;
    let path_buf = self.uri_to_path(path)?;
    let root_path = self.uri_to_path(&self.root)?;
    let walker = WalkBuilder::new(&path_buf)
      .filter_entry(move |entry| {
        entry.path().is_dir()
          || glob.is_match({
            entry
              .path()
              .strip_prefix(&root_path)
              .unwrap_or(entry.path())
          })
      })
      .build();

    let mut entries: im::Vector<Arc<dyn DirEntry>> = im::Vector::new();

    for entry_result in walker {
      match entry_result {
        | Ok(entry) => {
          let entry_path = entry.path();
          if let Ok(metadata) = entry.metadata()
            && !metadata.is_dir()
          {
            let entry_uri = Self::path_to_uri(entry_path)?;
            entries.push_back(Arc::new(PhysicalDirEntry {
              path: entry_uri,
              metadata,
            }) as Arc<dyn DirEntry>);
          }
        },
        | Err(err) => {
          return Err(
            Report::new(FsError::filesystem_error("Walk error"))
              .attach_printable(format!("Failed to walk directory: {err}"))
              .attach_printable(format!("Path: {path_buf:?}")),
          );
        },
      }
    }

    Ok(entries)
  }

  fn iter_files(
    &self,
  ) -> Result<Box<dyn Iterator<Item = (crate::Uri, Vec<u8>)> + '_>> {
    let root_path = self.uri_to_path(&self.root)?;

    let walker = WalkBuilder::new(&root_path)
      .hidden(false)
      .git_ignore(true)
      .build();

    let file_iter = walker.filter_map(move |entry| {
      let entry = entry.ok()?;
      let path = entry.path();

      if !entry.file_type()?.is_file() {
        return None;
      }

      let uri = PhysicalFileSystem::path_to_uri(path).ok()?;

      let content = std::fs::read(path).ok()?;

      Some((uri, content))
    });

    Ok(Box::new(file_iter))
  }
}

impl std::fmt::Debug for PhysicalFileSystem {
  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    write!(f, "PhysicalFileSystem({})", self.root)
  }
}

impl DirEntry for PhysicalDirEntry {
  fn path(&self) -> Uri {
    self.path.clone()
  }

  fn file_type(&self) -> FileType {
    if self.metadata.is_dir() {
      FileType::Directory
    } else {
      FileType::File
    }
  }

  fn metadata(&self) -> Result<Metadata> {
    Ok(PhysicalFileSystem::convert_metadata(self.metadata.clone()))
  }

  fn entry(&mut self, _entry: Box<dyn DirEntry>) {
    // Not implemented for physical file system - no-op
  }

  fn write(&mut self, path: &Uri, data: &[u8]) -> Result<()> {
    if !self.is_dir() {
      return Err(
        Report::new(FsError::invalid_operation(
          "Cannot write to a non-directory entry",
        ))
        .attach_printable(format!("Path: {}", self.path)),
      );
    }
    let fs = PhysicalFileSystem::new(self.path.clone())?;
    fs.write(path, data)
  }
}

impl FileHandle for PhysicalFileHandle {
  fn metadata(&self) -> Result<Metadata> {
    let metadata = self.file.metadata().map_err(|e| {
      e.to_fs_error()
        .attach_printable("Failed to get file handle metadata")
    })?;
    Ok(PhysicalFileSystem::convert_metadata(metadata))
  }
}

impl Read for PhysicalFileHandle {
  fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
    self.file.read(buf)
  }
}

impl Write for PhysicalFileHandle {
  fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
    self.file.write(buf)
  }

  fn flush(&mut self) -> std::io::Result<()> {
    self.file.flush()
  }
}