#![allow(clippy::pattern_type_mismatch)]
mod fs;
mod ui;
pub(crate) use {
fs::File,
ui::{Dimensions, UserAction},
};
use {
crate::app::Document,
clap::ArgMatches,
core::{
convert::TryFrom,
sync::atomic::{AtomicBool, Ordering},
},
docuglot::{ClientStatement, ServerStatement, Tongue},
fehler::{throw, throws},
fs::{
create_file_system, ConsumeFileError, FileCommand, FileCommandProducer, FileConsumer,
FileError, RootDirError,
},
log::error,
lsp_types::ShowMessageRequestParams,
market::{
channel::{CrossbeamConsumer, CrossbeamProducer, WithdrawnDemand, WithdrawnSupply},
vec::{Collector, Distributor},
ConsumeFailure, ConsumeFault, Consumer, ProduceFailure, Producer,
},
parse_display::Display as ParseDisplay,
starship::{context::Context, print},
std::io::{self, ErrorKind},
thiserror::Error as ThisError,
toml::{value::Table, Value},
ui::{
CreateTerminalError, DisplayCmd, DisplayCmdFailure, Terminal, UserActionConsumer,
UserActionFailure,
},
};
#[derive(Debug, ThisError)]
pub enum CreateInterfaceError {
#[error("current working directory is invalid: {0}")]
WorkingDir(#[from] io::Error),
#[error("unable to create URL of root directory: {0}")]
RootDir(#[from] RootDirError),
#[error("home directory of current user is unknown")]
HomeDir,
#[error(transparent)]
Terminal(#[from] CreateTerminalError),
#[error(transparent)]
CreateFile(#[from] FileError),
}
#[derive(Debug, market::ProduceFault, ThisError)]
pub enum ProduceOutputError {
#[error("")]
File(#[from] FileError),
#[error("{0}")]
CreateFile(#[from] CreateFileError),
#[error("{0}")]
UiProduce(#[from] DisplayCmdFailure),
#[error(transparent)]
Withdrawn(#[from] WithdrawnDemand),
}
#[derive(Debug, ThisError)]
pub enum CreateFileError {
#[error(transparent)]
Read(#[from] ProduceFailure<FileError>),
}
#[derive(Debug, ThisError)]
#[error("failed to read `{file}`: {error:?}")]
struct ReadFileError {
error: ErrorKind,
file: String,
}
#[derive(Debug, ThisError)]
enum Glitch {
#[error("config file invalid format: {0}")]
ConfigFormat(#[from] toml::de::Error),
}
#[derive(Debug, ConsumeFault, ThisError)]
pub(crate) enum ConsumeInputIssue {
#[error("")]
Quit,
#[error("")]
Error(#[from] ConsumeInputError),
}
#[derive(Debug, ConsumeFault, ThisError)]
pub enum ConsumeInputError {
#[error("")]
Ui(#[from] UserActionFailure),
#[error("")]
File(#[from] ConsumeFileError),
#[error(transparent)]
Withdrawn(#[from] WithdrawnSupply),
}
#[derive(Debug, ThisError)]
pub(crate) enum InvalidFileStringError {
#[error("")]
RootDir(#[from] RootDirError),
}
struct InternalTerminal(Terminal);
impl Producer for InternalTerminal {
type Good = DisplayCmd;
type Failure = ProduceFailure<ProduceOutputError>;
#[throws(Self::Failure)]
fn produce(&self, good: Self::Good) {
self.0.produce(good).map_err(ProduceFailure::map_fault)?
}
}
struct InternalUserActionConsumer(UserActionConsumer);
impl Consumer for InternalUserActionConsumer {
type Good = UserAction;
type Failure = ConsumeFailure<ConsumeInputError>;
#[throws(Self::Failure)]
fn consume(&self) -> Self::Good {
self.0.consume().map_err(ConsumeFailure::map_fault)?
}
}
struct InternalLspProducer(CrossbeamProducer<ClientStatement>);
impl Producer for InternalLspProducer {
type Good = ClientStatement;
type Failure = ProduceFailure<ProduceOutputError>;
#[throws(Self::Failure)]
fn produce(&self, good: Self::Good) {
self.0.produce(good).map_err(ProduceFailure::map_fault)?
}
}
struct InternalLspConsumer(CrossbeamConsumer<ServerStatement>);
impl Consumer for InternalLspConsumer {
type Good = ServerStatement;
type Failure = ConsumeFailure<ConsumeInputError>;
#[throws(Self::Failure)]
fn consume(&self) -> Self::Good {
self.0.consume().map_err(ConsumeFailure::map_fault)?
}
}
struct InternalFileProducer(FileCommandProducer);
impl Producer for InternalFileProducer {
type Good = FileCommand;
type Failure = ProduceFailure<ProduceOutputError>;
#[throws(Self::Failure)]
fn produce(&self, good: Self::Good) {
self.0
.produce(good)
.map_err(|error| ProduceFailure::Fault(error.into()))?
}
}
struct InternalFileConsumer(FileConsumer);
impl Consumer for InternalFileConsumer {
type Good = File;
type Failure = ConsumeFailure<ConsumeInputError>;
#[throws(Self::Failure)]
fn consume(&self) -> Self::Good {
self.0.consume().map_err(ConsumeFailure::map_fault)?
}
}
#[derive(Debug)]
pub(crate) struct Interface {
consumers: Collector<Input, ConsumeInputError>,
producers: Distributor<Output, ProduceOutputError>,
tongue: Tongue,
has_quit: AtomicBool,
}
impl Interface {
#[throws(CreateInterfaceError)]
pub(crate) fn new(initial_file: Option<String>) -> Self {
let user_interface = InternalTerminal(Terminal::new()?);
let mut consumers = Collector::new();
let mut producers = Distributor::new();
let (file_command_producer, file_consumer) = create_file_system()?;
let (lsp_producer, lsp_consumer, tongue) = docuglot::init(file_command_producer.root_dir());
if let Some(file) = initial_file {
file_command_producer.produce(FileCommand::Read { path: file })?
}
consumers.push(InternalUserActionConsumer(UserActionConsumer));
consumers.push(InternalLspConsumer(lsp_consumer));
consumers.push(InternalFileConsumer(file_consumer));
producers.push(InternalLspProducer(lsp_producer));
producers.push(user_interface);
producers.push(InternalFileProducer(file_command_producer));
let interface = Self {
consumers,
producers,
tongue,
has_quit: AtomicBool::new(false),
};
interface
}
pub(crate) fn join(&self) {
self.tongue.join()
}
}
impl Consumer for Interface {
type Good = Input;
type Failure = ConsumeFailure<ConsumeInputIssue>;
#[throws(Self::Failure)]
fn consume(&self) -> Self::Good {
match self.consumers.consume() {
Ok(input) => input,
Err(market::ConsumeFailure::Fault(failure)) => {
throw!(market::ConsumeFailure::Fault(ConsumeInputIssue::Error(
failure
)))
}
Err(market::ConsumeFailure::EmptyStock) => {
if self.has_quit.load(Ordering::Relaxed) {
throw!(market::ConsumeFailure::Fault(ConsumeInputIssue::Quit));
} else {
throw!(market::ConsumeFailure::EmptyStock);
}
}
}
}
}
impl Producer for Interface {
type Good = Output;
type Failure = ProduceFailure<ProduceOutputError>;
#[throws(Self::Failure)]
fn produce(&self, output: Self::Good) {
self.producers.produce(output.clone())?;
match output {
Output::OpenFile { .. }
| Output::UpdateView { .. }
| Output::EditDoc { .. }
| Output::UpdateHeader
| Output::Question { .. }
| Output::Command { .. } => {}
Output::Quit => {
self.has_quit.store(true, Ordering::Relaxed);
}
}
}
}
#[derive(Debug, ThisError)]
#[error("while converting `{0}` to a URL")]
struct UrlError(String);
#[derive(Debug)]
pub(crate) enum Input {
File(File),
User(UserAction),
Lsp(ServerStatement),
}
impl From<File> for Input {
#[inline]
fn from(value: File) -> Self {
Self::File(value)
}
}
impl From<ServerStatement> for Input {
#[inline]
fn from(value: ServerStatement) -> Self {
Self::Lsp(value)
}
}
impl From<UserAction> for Input {
#[inline]
fn from(value: UserAction) -> Self {
Self::User(value)
}
}
#[derive(Clone, Debug, ParseDisplay)]
pub(crate) enum Output {
#[display("Get file `{path}`")]
OpenFile {
path: String,
},
#[display("")]
EditDoc {
doc: Document,
edit: DocEdit,
},
#[display("")]
UpdateView {
rows: Vec<String>,
},
#[display("")]
UpdateHeader,
#[display("")]
Question {
request: ShowMessageRequestParams,
},
#[display("")]
Command {
command: String,
},
#[display("")]
Quit,
}
impl TryFrom<Output> for FileCommand {
type Error = TryIntoFileCommandError;
#[inline]
#[throws(Self::Error)]
fn try_from(value: Output) -> Self {
match value {
Output::OpenFile { path } => Self::Read { path },
Output::Command { .. }
| Output::EditDoc { .. }
| Output::UpdateHeader
| Output::UpdateView { .. }
| Output::Question { .. }
| Output::Quit => throw!(TryIntoFileCommandError::InvalidOutput),
}
}
}
impl TryFrom<Output> for ClientStatement {
type Error = TryIntoProtocolError;
#[inline]
#[throws(Self::Error)]
fn try_from(value: Output) -> Self {
match value {
Output::EditDoc { doc, edit } => match edit {
DocEdit::Open { .. } => ClientStatement::open_doc(doc.into()),
DocEdit::Close => ClientStatement::close_doc(doc.into()),
DocEdit::Update => throw!(TryIntoProtocolError::InvalidOutput),
},
Output::OpenFile { .. }
| Output::Command { .. }
| Output::UpdateHeader
| Output::UpdateView { .. }
| Output::Question { .. }
| Output::Quit => throw!(TryIntoProtocolError::InvalidOutput),
}
}
}
impl TryFrom<Output> for DisplayCmd {
type Error = TryIntoDisplayCmdError;
#[inline]
#[throws(Self::Error)]
fn try_from(value: Output) -> Self {
match value {
Output::EditDoc { doc, edit } => match edit {
DocEdit::Open { .. } | DocEdit::Update => DisplayCmd::Rows { rows: doc.rows() },
DocEdit::Close => throw!(TryIntoDisplayCmdError::InvalidOutput),
},
Output::UpdateView { rows } => DisplayCmd::Rows { rows },
Output::Question { request } => DisplayCmd::Rows {
rows: vec![request.message],
},
Output::Command { command } => DisplayCmd::Rows {
rows: vec![command],
},
Output::UpdateHeader => {
let mut context = Context::new(ArgMatches::new());
if let Some(mut config) = context.config.config.clone() {
if let Some(table) = config.as_table_mut() {
let _ = table.insert("add_newline".to_string(), Value::Boolean(false));
if let Some(line_break) = table
.entry("line_break")
.or_insert(Value::Table(Table::new()))
.as_table_mut()
{
let _ = line_break.insert("disabled".to_string(), Value::Boolean(true));
}
}
context.config.config = Some(config);
}
DisplayCmd::Header {
header: print::get_prompt(context),
}
}
Output::OpenFile { .. } | Output::Quit => throw!(TryIntoDisplayCmdError::InvalidOutput),
}
}
}
#[derive(Debug, ThisError)]
pub(crate) enum TryIntoFileCommandError {
#[error("")]
InvalidOutput,
}
#[derive(Debug, ThisError)]
pub(crate) enum TryIntoDisplayCmdError {
#[error("")]
InvalidOutput,
}
#[derive(Clone, Copy, Debug, ThisError)]
pub(crate) enum TryIntoProtocolError {
#[error("")]
InvalidOutput,
}
#[derive(Clone, Copy, Debug)]
pub(crate) enum DocEdit {
Open {
version: i64,
},
Update,
Close,
}
#[derive(Clone, Debug)]
pub(crate) struct DocChange {
new_text: String,
version: i64,
}