Giter Site home page Giter Site logo

kkharji / xbase Goto Github PK

View Code? Open in Web Editor NEW
522.0 11.0 18.0 31.81 MB

Develop Apple software products within your favorite editor.

License: MIT License

Makefile 0.54% Rust 62.10% Lua 16.10% TypeScript 20.44% Vim Script 0.82%
apple codegen ide ios lua macos nvim rust xcode vscode

xbase's Introduction

Logo

An Xcode replacement-ish development environment that aims to be your reliable Xcode alternative to develop exciting new apple software products 🚀.

Table of Content

👁 Overview

XBase enables you to build, watch, and run xcode products as well as swift packages from within your favorite editor. It supports running products on iOS, watchOS and tvOS simulators, along with real-time logging, and some lsp features such as auto-completion and code navigation. (🌟 Features).

Furthermore, XBase has built-in support for a variety of Xcode project generators, which allow you to avoid launching Xcode or manually editing '*.xcodeproj' anytime you add or remove files. We strongly advise you to use one ... at least till XBase supports adding/removing files and folders, along with other requirements. (💆 Generators)

Please be aware that XBase is still WIP, so don't hesitate to report bugs, ask questions or suggest new exciting features.

🌝 Motivation

I chose to dive into iOS/macOS app development after purchasing an M1 MacBook. However, coming from vim/shell environment and being extremely keyboard oriented, I couldn't handle the transition to a closed sourced, opinionated, mouse-driven development environment. I've considered alternatives like XVim2 and the built-in vim emulator, however still, I'd catch myself frequently hunting for my mouse.

As a long-time vim user who has previously developed a several lua/nvim plugins, I decided to invest some effort in simplifying my development workflow for producing 'xOS' products.

🌟 Features

  • Auto-Completion and Code navigation
    Auto-generate compilation database on directory changes + a custom build server that assists sourcekit-lsp in providing code navigation and auto-completion for project symbols.
  • Multi-nvim instance support
    Multiple nvim instance support without process duplications and shared state. For instance, you can stop a watch service that was being run from a different instance.
  • Auto-start/stop main background daemon
    Daemon will start and stop automatically based on the number of connected client instances.
  • Multi Target/Project Support
    Work on multiple projects at one nvim instance at the same time.
  • Simulator Support
    Run your products on simulators relative to your target's platform. (+ watch build and ran on change)
  • Runtime/Build Logging
    Real-time logging of build logs and 'print()' commands
  • Statusline Support
    Global variable to update statusline with build/run commands, see Statusline
  • Zero Footprint
    Light resource usage. I've been using XBase for a while; it typically uses 0.1 percent RAM and 0 percent CPU.
  • Multi XcodeProj Generator Support
    Auto-generate xcodeproj, when it doesn't exists, generator config files a updated or new files/directories added or removed.
  • Swift Package Support
    Auto-generate when .build folder doesn't exists, Package.swift file is updated or new files or directories are added or removed.

💆 Generators

XBase primarily supports two project generators: XcodeGen and Tuist.

XcodeGen is recommended if you are just starting started with xcodeproj generators since it is considerably simpler with a yml-based configuration language. Having said that, Tuist is more powerful and packed with features, of which xcodeproj generation is but one.

XBase's support for generators is available in the following forms:

  • Identification.
  • Auto-generate xcodeproj if you haven't haven't generate it by hand.
  • Auto-generate xcodeproj when you edit the generator config files.
  • Auto-compile project when xcodeproj get regenerated.
  • Code Completion and navigation (#tuist)

Limitations

  • No support for custom named yml config files, only project.yml.

Other Generators

With current XBase architecture, it should be pretty easy to add support for yet another awesome xcodeproj generator. feel free to get started with [CONTRIBUTING.md] or open a github issue

🛠 Requirements

Shared

  • rust ^1.60 compile project locally

Neovim

Vscode

TODO

🦾 Installation

To install XBase on your system you need run make install. This will run cargo build --release and resulting binrary to ~/.local/share/xbase/.

Neovim

With packer

use {
  'xbase-lab/xbase',
    run = 'make install', -- or "make install && make free_space" (not recommended, longer build time)
    requires = {
      "neovim/nvim-lspconfig",
      -- "nvim-telescope/telescope.nvim", -- optional
      -- "nvim-lua/plenary.nvim", -- optional/requirement of telescope.nvim
      -- "stevearc/dressing.nvim", -- optional (in case you don't use telescope but something else)
    },
    config = function()
      require'xbase'.setup({})  -- see default configuration bellow
    end
}
" Plug 'nvim-telescope/telescope.nvim' " optional
" Plug 'nvim-lua/plenary.nvim' " optional/requirement of telescope
" Plug 'stevearc/dressing.nvim' " optional/in case you don't use telescope but use something else
Plug 'neovim/nvim-lspconfig'
Plug 'xbase-lab/xbase', { 'do': 'make install' }
lua require'xbase'.setup()

With dein

" call dein#add('nvim-telescope/telescope.nvim') " optional
" call dein#add('nvim-lua/plenary.nvim') " optional/requirement of telescope
" call dein#add('stevearc/dressing.nvim') " optional/in case you don't use telescope but use something else
call dein#add('neovim/nvim-lspconfig')
call dein#add('xbase-lab/xbase', { 'build': 'make install' })
lua require'xbase'.setup()

NOTE: You need to setup sourcekit-lsp (see sourcekit-setup) and consider adding more file to root patterns

Vscode

TODO

🎮 Usage

Neovim

TLDR:

  • Install XBase
  • run require'xbase'.setup({ --[[ see default configuration ]] })
  • Open xcodeproj codebase.
  • Wait for first time project setup finish.
  • Start coding
  • Use available actions which can be configure with shortcuts bellow

When you start a neovim instance with a root that contains project.yml, Project.swift, or *.xcodeproj, the daemon server will auto-start if no instance is running, and register the project once for recompile-watch. To communicate with your daemon, checkout the configurable shortcuts.

Statusline

XBase provide feline provider, other statusline plugins support are welcomed. However, using vim.g.xbase_watch_build_status you can easily setup statusline indicators.

require("xbase.statusline").feline() -- append to feline setup function

Vscode

TODO

⚙️ Defaults

Neovim

-- NOTE: Defaults
{
  --- Log level. Set it to ERROR to ignore everything
  log_level = vim.log.levels.DEBUG,
  --- Options to be passed to lspconfig.nvim's sourcekit setup function.
  --- Setting this to {} is sufficient, However, it is strongly recommended to use on_attach key to setup custom mappings
  --- {
  --- cmd = { "sourcekit-lsp", "--log-level", "error" },
  --- filetypes = { "swift" },
  --- root_dir = pattern("Package.swift", ".git", "project.yml", "Project.swift"),
  --- }
  sourcekit = nil, -- Disabled by default (xbase will not call it for you)
  --- Statusline provider configurations
  statusline = {
    watching = { icon = "", color = "#1abc9c" },
    device_running = { icon = "", color = "#4a6edb" },
    success = { icon = "", color = "#1abc9c" },
    failure = { icon = "", color = "#db4b4b" },
  },
  --- Simulators to only include.
  --- run `xcrun simctl list` to get a full list of available simulator
  --- If the list is empty then all simulator available  will be included
  simctl = {
    iOS = {
      -- "iPhone 13 Pro", --- only use this devices
    },
    watchOS = {}, -- all available devices
    tvOS = {}, -- all available devices
  },
  --- Log buffer configurations
  log_buffer = {
    --- Whether toggling the buffer should auto focus to it?
    focus = true,
    --- Split Log buffer height
    height = 20,
    --- Vsplit Log buffer width
    width = 75,
    --- Default log buffer direction: { "horizontal", "vertical" }
    default_direction = "horizontal",
  },
  --- Mappings
  mappings = {
    --- Whether xbase mapping should be disabled.
    enable = true,
    --- Open build picker. showing targets and configuration.
    build_picker = "<leader>b", --- set to 0 to disable
    --- Open run picker. showing targets, devices and configuration
    run_picker = "<leader>r", --- set to 0 to disable
    --- Open watch picker. showing run or build, targets, devices and configuration
    watch_picker = "<leader>s", --- set to 0 to disable
    --- A list of all the previous pickers
    all_picker = "<leader>ef", --- set to 0 to disable
    --- horizontal toggle log buffer
    toggle_split_log_buffer = "<leader>ls",
    --- vertical toggle log buffer
    toggle_vsplit_log_buffer = "<leader>lv",
  },
}

Vscode

TODO

🩺 Debugging

Sometimes xcodebuild acts up and things might break, the first step to find the root cause is to check logs. The following is how you can have a stream of logs in your terminal.

# Daemon logs
tail -f /tmp/xbase.log
# Build Server logs
tail -f /tmp/xbase-build-server.log

In case, you need to manually stop servers:

killall xbase xbase-sourcekit-helper

xbase's People

Contributors

blackjacx avatar dependabot[bot] avatar github-actions[bot] avatar kkharji avatar pierrecapo avatar sainttttt avatar spenceryr avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

xbase's Issues

Cleanup get_ignore_patterns and decrease duplications

https://github.com/tami5/XcodeBase.nvim/blob/2d1123e26527e95776e18b4fc28a50013d63ba7c/src/util/watch.rs#L40

//! Function to watch file system
//!
//! Mainly used for creation/removal of files and editing of xcodegen config.
use crate::daemon::DaemonState;
use anyhow::Result;
use notify::{Error, Event, RecommendedWatcher, RecursiveMode, Watcher};
use std::{future::Future, path::PathBuf, sync::Arc, time::SystemTime};
use std::{path::Path, time::Duration};
use tokio::sync::{mpsc, Mutex};
use tokio::task::JoinHandle;
use wax::{Glob, Pattern};

const COMPILE_START_MSG: &str = "echo 'xcodebase: ⚙ Regenerating compilation database ..'";
const COMPILE_SUCC_MESSAGE: &str = "echo 'xcodebase: ✅ Compilation database regenerated.'";

/// HACK: ignore seen paths.
///
/// Sometimes we get event for the same path, particularly
/// `ModifyKind::Name::Any` is omitted twice for the new path
/// and once for the old path.
///
/// This will compare last_seen with path, updates `last_seen` if not match,
/// else returns true.
#[cfg(feature = "async")]
pub async fn should_ignore(last_seen: Arc<Mutex<String>>, path: &str) -> bool {
    // HACK: Always return false for project.yml
    let path = path.to_string();
    if path.contains("project.yml") {
        return false;
    }
    let mut last_seen = last_seen.lock().await;
    if last_seen.to_string() == path {
        return true;
    } else {
        *last_seen = path;
        return false;
    }
}

// TODO: Cleanup get_ignore_patterns and decrease duplications
#[cfg(feature = "daemon")]
pub async fn get_ignore_patterns(state: DaemonState, root: &String) -> Vec<String> {
    let mut patterns: Vec<String> = vec![
        "**/.git/**",
        "**/*.xcodeproj/**",
        "**/.*",
        "**/build/**",
        "**/buildServer.json",
    ]
    .iter()
    .map(|e| e.to_string())
    .collect();

    // Note: Add extra ignore patterns to `ignore` local config requires restarting daemon.
    if let Some(ws) = state.lock().await.workspaces.get(root) {
        if let Some(extra_patterns) = ws.get_ignore_patterns() {
            patterns.extend(extra_patterns);
        }
    }

    patterns
}

pub fn new<F, Fut>(
    root: String,
    state: DaemonState,
    event_handler: F,
) -> JoinHandle<anyhow::Result<()>>
where
    F: Fn(DaemonState, String, PathBuf, Event, Arc<Mutex<String>>, Arc<Mutex<SystemTime>>) -> Fut
        + Send
        + 'static,
    Fut: Future<Output = anyhow::Result<bool>> + Send,
{
    use notify::Config::NoticeEvents;
    let debounce = Arc::new(Mutex::new(SystemTime::now()));

    tokio::spawn(async move {
        let (tx, mut rx) = mpsc::channel(1);
        let mut watcher = RecommendedWatcher::new(move |res: Result<Event, Error>| {
            if let Ok(event) = res {
                if let Err(err) = tx.blocking_send(event) {
                    #[cfg(feature = "logging")]
                    tracing::error!("Fail send event {err}");
                };
            } else {
                tracing::error!("Watch Error: {:?}", res);
            };
        })?;

        // NOTE: should watch for registered directories only?
        watcher.watch(Path::new(&root), RecursiveMode::Recursive)?;
        watcher.configure(NoticeEvents(true))?;

        // HACK: ignore seen paths.
        let last_seen = Arc::new(Mutex::new(String::default()));
        // HACK: convert back to Vec<&str> for Glob to work.
        let patterns = get_ignore_patterns(state.clone(), &root).await;
        let patterns = patterns.iter().map(AsRef::as_ref).collect::<Vec<&str>>();
        let ignore = match wax::any::<Glob, _>(patterns) {
            Ok(i) => i,
            Err(err) => {
                anyhow::bail!("Fail to generate ignore glob: {err}")
            }
        };

store log bufnr somewhere in vim state

https://github.com/tami5/XcodeBase.nvim/blob/8430976e4bb11be1fc0dda0e486e5ac4ceb47763/src/nvim/logbuffer.rs#L22

use super::*;

pub struct NvimLogBuffer {
    pub bufnr: i64,
}

pub struct BulkLogRequest<'a, S: Stream<Item = String> + Unpin> {
    pub nvim: &'a Nvim,
    pub title: &'a str,
    pub direction: Option<BufferDirection>,
    pub stream: S,
    pub clear: bool,
    pub open: bool,
}

impl NvimLogBuffer {
    pub async fn new(nvim: &NvimConnection) -> Result<Self> {
        let buf = nvim.create_buf(false, true).await?;

        buf.set_name("[Xcodebase Logs]").await?;
        buf.set_option("filetype", "xcodebuildlog".into()).await?;
        // TODO: store log bufnr somewhere in vim state

        let bufnr = buf.get_number().await?;

        Self { bufnr }.pipe(Ok)
    }

    pub async fn bulk_append<'a, S: Stream<Item = String> + Unpin>(
        &self,
        mut req: BulkLogRequest<'a, S>,
    ) -> Result<()> {
        req.nvim
            .exec("let g:xcodebase_watch_build_status='running'", false)
            .await?;
        let title = format!(
            "[{}] ------------------------------------------------------",
            req.title
        );
        let buf = Buffer::new(self.bufnr.into(), req.nvim.nvim.clone());

        if req.clear {
            buf.set_lines(0, -1, false, vec![]).await?;
        }

        let mut c = match buf.line_count().await? {
            1 => 0,
            count => count,
        };

        // TODO(nvim): build log correct height
        // TODO(nvim): make auto clear configurable
        let command = match BufferDirection::get_window_direction(
            req.nvim,
            req.direction,
            self.bufnr,
        )
        .await
        {
            Ok(open_command) => open_command,
            Err(e) => {
                tracing::error!("Unable to convert value to string {e}");
                BufferDirection::Horizontal.to_nvim_command(self.bufnr)
            }
        };
        let mut win: Option<Window<Connection>> = None;
        if req.open {
            req.nvim.exec(&command, false).await?;
            req.nvim.exec("setl nu nornu so=9", false).await?;
            win = Some(req.nvim.get_current_win().await?);
            req.nvim.exec("wincmd w", false).await?;
        }

        buf.set_lines(c, c + 1, false, vec![title]).await?;
        c += 1;
        let mut success = false;

        while let Some(line) = req.stream.next().await {
            if line.contains("Succeed") {
                success = true;
            }
            buf.set_lines(c, c + 1, false, vec![line]).await?;
            c += 1;
            if req.open {
                win.as_ref().unwrap().set_cursor((c, 0)).await?;
            }
        }

        if success {
            req.nvim
                .exec("let g:xcodebase_watch_build_status='success'", false)
                .await?;
        } else {
            req.nvim
                .exec("let g:xcodebase_watch_build_status='failure'", false)
                .await?;
            if !req.open {
                req.nvim.exec(&command, false).await?;
                req.nvim.get_current_win().await?.set_cursor((c, 0)).await?;
                req.nvim.exec("call feedkeys('zt')", false).await?;
            }
        }

        Ok(())
    }
}

Support projects with .xproj as well as xcworkspace

if !err.is_cancelled() {

error!("handler is not cancelled for {:#?}", self);

}

}

https://github.com/tami5/XcodeBase.nvim/blob/8430976e4bb11be1fc0dda0e486e5ac4ceb47763/src/state/daemon.rs#L52

use crate::{
    daemon::{WatchStart, Workspace},
    nvim::Client,
    watchers::{
        event_handler, new_watch_handler, recompile_handler, ProjectWatchers, TargetWatchers,
    },
};
use anyhow::Result;
use std::collections::HashMap;
use tap::Pipe;
use tracing::{error, info};

use super::set_watch_script;

/// Main state
#[derive(Default, Debug)]
pub struct DaemonStateData {
    /// Managed workspaces
    pub workspaces: HashMap<String, Workspace>,
    /// Connected clients process id
    pub clients: Vec<i32>,
    /// project recompile watcher
    pub build_watchers: ProjectWatchers,
    /// target recompile watcher
    pub target_watchers: TargetWatchers,
}

pub type DaemonState = std::sync::Arc<tokio::sync::Mutex<DaemonStateData>>;

impl DaemonStateData {
    pub fn get_workspace(&self, root: &str) -> Result<&Workspace> {
        match self.workspaces.get(root) {
            Some(o) => Ok(o),
            None => anyhow::bail!("No workspace for {}", root),
        }
    }

    pub fn get_mut_workspace(&mut self, root: &str) -> Result<&mut Workspace> {
        match self.workspaces.get_mut(root) {
            Some(o) => Ok(o),
            None => anyhow::bail!("No workspace for {}", root),
        }
    }

    pub async fn add_workspace(
        &mut self,
        root: &str,
        pid: i32,
        address: &str,
        state: DaemonState,
    ) -> Result<()> {
        // TODO: Support projects with .xproj as well as xcworkspace

        if self.workspaces.contains_key(root) {
            let ws = self.get_mut_workspace(root).unwrap();
            return ws.add_nvim_client(pid, address).await;
        }

        let workspace = Workspace::new(root, pid, address).await?;
        let root = root.to_string();

        self.watch(&root, None, state).await?;
        self.workspaces.insert(root, workspace);

        tracing::trace!("{:#?}", self);

        Ok(())
    }

    // Remove remove client from workspace and the workspace if it's this client was the last one.
    pub async fn remove_workspace(&mut self, root: &str, pid: i32) -> Result<()> {
        let mut name = None;

        if let Some(ws) = self.workspaces.get_mut(root) {
            let clients_len = ws.remove_client(pid);
            clients_len
                .eq(&0)
                .then(|| name = ws.project.name().to_string().into());
        } else {
            error!("'{root}' is not a registered workspace!");
        }

        if let Some(name) = name {
            info!("Dropping [{}] {:?}", name, root);

            if self.build_watchers.contains_key(root) {
                self.stop_watch(root, None).await;
            }

            self.workspaces.remove(root);
        }

        Ok(())
    }

    /// Stop a watch service
    async fn stop_watch(&mut self, root: &str, client: Option<Client>) -> Result<()> {
        if client.is_some() {
            self.validate(client).await?;
        } else {
            if let Some(handle) = self.build_watchers.get(root) {
                handle.abort();
                tracing::debug!("Project Watch service stopeed");
                self.build_watchers.remove(root);
            }
        }
        Ok(())
    }

    pub async fn validate(&mut self, client: Option<Client>) -> Result<()> {
        if let Some(client) = client {
            let mut dropped_targets = vec![];
            let root = client.root.clone();

            self.target_watchers.retain(|target, (req, handle)| {
                if req.client.root == client.root && req.client.pid == client.pid {
                    handle.abort();
                    // if let Err(err) = handle.await {
                    //     if !err.is_cancelled() {
                    //         error!("handler is not cancelled for {:#?}", self);
                    //     }
                    // }
                    dropped_targets.push(target.clone());
                    true
                } else {
                    false
                }
            });

            let ws = self.get_workspace(&root)?;
            let scripts = dropped_targets
                .iter()
                .map(|target| set_watch_script(&client.root, target, false))
                .collect::<Vec<String>>();

            for (_, nvim) in ws.clients.iter() {
                for script in scripts.iter() {
                    nvim.exec_lua(script, vec![]).await?;
                }
            }
        } else {
            use crate::util::proc_exists;
            self.clients
                .retain(|pid| proc_exists(pid, || info!("Removing {pid}")));
            self.workspaces
                .iter_mut()
                .for_each(|(_, ws)| ws.ensure_active_clients())
        }
        Ok(())
    }

    pub async fn watch(
        &mut self,
        root: &str,
        watch_req: Option<WatchStart>,
        state: DaemonState,
    ) -> Result<()> {
        if let Some(watch_req) = watch_req {
            let ref mut watchers = self.target_watchers;
            let target = watch_req.request.target.clone();

            tracing::info!("Watch target [{}]", watch_req.request.target);
            new_watch_handler(root.into(), state, event_handler, target.clone().into())
                .pipe(|handler| watchers.insert(target.clone(), (watch_req, handler)));

            let ws = self.get_mut_workspace(&root)?;
            let update_script = set_watch_script(&root, &target, true);

            for (_, nvim) in ws.clients.iter() {
                nvim.exec_lua(&update_script, vec![]).await?;
            }
        } else {
            let ref mut watchers = self.build_watchers;
            new_watch_handler(root.into(), state, recompile_handler, None)
                .pipe(|handler| watchers.insert(root.into(), handler));
        }

        Ok(())
    }
}

Implement run command

Also, it might be important to pick which target/paltform to run under. This is currently just

either with a simulator or not assuming only the use case won't include

macos apps, which is wrong

https://github.com/tami5/XcodeBase.nvim/blob/8694c471ad68c99ff400c768f65ec17fa8fe9ead/src/daemon/command/run.rs#L9

    _simulator: bool,
}

// TODO: Implement run command
//
// Also, it might be important to pick which target/paltform to run under. This is currently just
// either with a simulator or not assuming only the use case won't include
// macos apps, which is wrong
#[cfg(feature = "daemon")]
#[async_trait::async_trait]
impl crate::DaemonCommandExt for Run {
    async fn handle(&self, _state: crate::state::SharedState) -> Result<()> {
        tracing::info!("Run command");
        Ok(())
    }
}

impl TryFrom<Vec<&str>> for Run {
    type Error = anyhow::Error;

    fn try_from(args: Vec<&str>) -> Result<Self, Self::Error> {
        let _simulator = args.get(0).unwrap_or(&"").parse::<bool>().unwrap_or(false);
        Ok(Self { _simulator })
    }
}

impl Run {
    pub const KEY: &'static str = "run";

    pub fn request(with_simulator: bool) -> Result<()> {
        crate::Daemon::execute(&[Self::KEY, &with_simulator.to_string()])
    }
}

#[cfg(feature = "lua")]
impl Run {
    pub fn lua(lua: &mlua::Lua, with_simulator: bool) -> mlua::Result<()> {
        use crate::LuaExtension;
        lua.trace(&format!("Run command called"))?;
        Self::request(with_simulator).map_err(mlua::Error::external)
    }
}

[nvim] build log auto scroll

https://github.com/tami5/XcodeBase.nvim/blob/a9eede75989425943de53943e00fc1faa23f6d97/src/daemon/nvim.rs#L84

    pub async fn log_warn(&self, scope: &str, msg: impl ToString) -> Result<()> {
        self.log("warn", scope, msg).await
    }

    pub async fn log_to_buffer(
        &self,
        title: &str,
        direction: WindowType,
        mut stream: impl tokio_stream::Stream<Item = String> + Unpin,
        clear: bool,
    ) -> Result<()> {
        let title = format!("[ {title} ]: ----> ");
        let buffer = Buffer::new(self.log_bufnr.into(), self.nvim.clone());

        if clear {
            buffer.set_lines(0, -1, false, vec![]).await?;
        }

        let mut c = match buffer.line_count().await? {
            1 => 0,
            count => count,
        };

        // TODO(nvim): build log control what direction to open buffer
        // TODO(nvim): build log correct width
        // TODO(nvim): build log auto scroll
        let command = match direction {
            // TOOD: build log float
            WindowType::Float => format!("sbuffer {}", self.log_bufnr),
            WindowType::Vertical => format!("vert sbuffer {}", self.log_bufnr),
            WindowType::Horizontal => format!("sbuffer {}", self.log_bufnr),
        };

        self.exec(&command, false).await?;

        buffer.set_lines(c, c + 1, false, vec![title]).await?;
        c += 1;

        while let Some(line) = stream.next().await {
            buffer.set_lines(c, c + 1, false, vec![line]).await?;
            c += 1
        }

        Ok(())
    }
}

impl std::fmt::Debug for Nvim {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_tuple("Nvim").finish()
    }
}

impl ops::Deref for Nvim {
    type Target = Neovim<Compat<WriteHalf<Connection>>>;
    fn deref(&self) -> &Self::Target {
        &self.nvim
    }
}

Identify and check for a directory that ends with `.xcodeproj`

https://github.com/tami5/xbase.nvim/blob/38f14e4c3de8f6e4e847ebaddcb3300a20295540/src/compile.rs#L238

    Ok(())
}

#[cfg(feature = "daemon")]
pub async fn ensure_server_support<'a>(
    state: &'a mut tokio::sync::OwnedMutexGuard<crate::state::State>,
    name: &String,
    root: &std::path::PathBuf,
    path: Option<&std::path::PathBuf>,
) -> Result<bool> {
    use crate::{error::XcodeGenError, xcodegen};
    use tokio::fs::metadata;
    let compile_exists = metadata(root.join(".compile")).await.is_ok();

    if ensure_server_config(&root).await.is_err() {
        "fail to ensure build server configuration!"
            .pipe(|msg| state.clients.echo_err(&root, &name, msg))
            .await;
    }

    if let Some(path) = path {
        let generated = match xcodegen::regenerate(path, &root).await {
            Ok(generated) => generated,
            Err(e) => {
                state.clients.echo_err(&root, &name, &e.to_string()).await;
                return Err(e);
            }
        };

        if generated {
            state.projects.get_mut(root)?.update().await?
        }
    } else if compile_exists {
        return Ok(false);
    }

    if xcodegen::is_valid(&root) && path.is_none() {
        "⚙ generating xcodeproj ..."
            .pipe(|msg| state.clients.echo_msg(&root, &name, msg))
            .await;

        if let Some(err) = match xcodegen::generate(&root).await {
            Ok(status) => {
                if status.success() {
                    "setup: ⚙ generate xcodeproj ..."
                        .pipe(|msg| state.clients.echo_msg(&root, &name, msg))
                        .await;
                    None
                } else {
                    Some(XcodeGenError::XcodeProjUpdate(name.into()).into())
                }
            }
            Err(e) => Some(e),
        } {
            let msg = format!("fail to generate xcodeproj: {err}");
            state.clients.echo_err(&root, &name, &msg).await;
        }
    };

    // TODO(compile): check for .xcodeproj if project.yml is not generated
    if !compile_exists {
        "⚙ generating compile database .."
            .pipe(|msg| state.clients.echo_msg(&root, &name, msg))
            .await;
    }

    // The following command won't successed if this file doesn't exists
    if let Err(err) = update_compilation_file(&root).await {
        "setup: fail to regenerate compilation database!"
            .pipe(|msg| state.clients.echo_err(&root, &name, msg))
            .await;

        use crate::util::proc_exists;
        for (pid, client) in state.clients.iter() {
            if proc_exists(pid, || {}) {
                let mut logger = client.new_logger("Compile Error", name, &None);
                logger.set_running().await.ok();
                let ref win = Some(logger.open_win().await?);
                logger.log(err.to_string(), win).await.ok();
                logger.set_status_end(false, true).await.ok();
            }
        }

        return Err(err);
    }

    tracing::info!("Updated `{name}/.compile`");

    Ok(true)
}

[simctl] device might change outside state

https://github.com/tami5/xbase.nvim/blob/b7a947a97abca5c4ac33abe1d9407e0d23a40c1f/src/runner/simctl.rs#L35

use crate::{types::SimDevice, Error};

use super::*;

impl Runner {
    pub async fn run_with_simctl(self, settings: BuildSettings) -> Result<JoinHandle<Result<()>>> {
        let nvim = self.state.clients.get(&self.client.pid)?;
        let mut logger = nvim.new_logger("Run", &self.target, &self.direction);

        let app_id = settings.product_bundle_identifier;
        let path_to_app = settings.metal_library_output_dir;

        tracing::debug!("{app_id}: {:?}", path_to_app);

        logger.log_title().await?;
        logger.open_win().await?;

        let mut device = get_device(&self.state, self.udid)?;

        // NOTE: This is required so when neovim exist this should also exit
        let state = DAEMON_STATE.clone().lock_owned().await;

        tokio::spawn(async move {
            let nvim = state.clients.get(&self.client.pid)?;
            let ref mut logger = nvim.new_logger("Run", &self.target, &self.direction);

            logger.set_running().await?;

            device.try_boot(logger).await?;
            device.try_install(&path_to_app, &app_id, logger).await?;
            device.try_launch(&app_id, logger).await?;

            let mut state = DAEMON_STATE.clone().lock_owned().await;

            // TODO(simctl): device might change outside state
            state.devices.insert(device);

            // TODO: Remove and replace with app logs
            logger.set_status_end(true, false).await?;

            state.sync_client_state().await?;

            Ok(())
        })
        .pipe(Ok)
    }
}

fn get_device<'a>(state: &'a OwnedMutexGuard<State>, udid: Option<String>) -> Result<SimDevice> {
    if let Some(udid) = udid {
        state.devices.iter().find(|d| d.udid == udid).cloned()
    } else {
        None
    }
    .ok_or_else(|| Error::Run("udid not found!!".to_string()))
}

move xcodegen generate command to it's own module

https://github.com/tami5/XcodeBase.nvim/blob/a5c42fabcf8ba548b719e2ce1585ec32ddcc2444/src/state/workspace.rs#L124

           number of paths by default.
        */
        let xcodegen_path = dirs::home_dir().unwrap().join(".mint/bin/xcodegen");
        // TODO: move xcodegen generate command to it's own module
        let xcodegen = tokio::process::Command::new(xcodegen_path)
            .current_dir(self.root.clone())
            .stdout(std::process::Stdio::null())
            .arg("generate")
            .spawn()
            .expect("Failed to start xcodeGen.")

Remove and replace with app logs

https://github.com/tami5/xbase.nvim/blob/b7a947a97abca5c4ac33abe1d9407e0d23a40c1f/src/runner/simctl.rs#L38

use crate::{types::SimDevice, Error};

use super::*;

impl Runner {
    pub async fn run_with_simctl(self, settings: BuildSettings) -> Result<JoinHandle<Result<()>>> {
        let nvim = self.state.clients.get(&self.client.pid)?;
        let mut logger = nvim.new_logger("Run", &self.target, &self.direction);

        let app_id = settings.product_bundle_identifier;
        let path_to_app = settings.metal_library_output_dir;

        tracing::debug!("{app_id}: {:?}", path_to_app);

        logger.log_title().await?;
        logger.open_win().await?;

        let mut device = get_device(&self.state, self.udid)?;

        // NOTE: This is required so when neovim exist this should also exit
        let state = DAEMON_STATE.clone().lock_owned().await;

        tokio::spawn(async move {
            let nvim = state.clients.get(&self.client.pid)?;
            let ref mut logger = nvim.new_logger("Run", &self.target, &self.direction);

            logger.set_running().await?;

            device.try_boot(logger).await?;
            device.try_install(&path_to_app, &app_id, logger).await?;
            device.try_launch(&app_id, logger).await?;

            let mut state = DAEMON_STATE.clone().lock_owned().await;

            // TODO(simctl): device might change outside state
            state.devices.insert(device);

            // TODO: Remove and replace with app logs
            logger.set_status_end(true, false).await?;

            state.sync_client_state().await?;

            Ok(())
        })
        .pipe(Ok)
    }
}

fn get_device<'a>(state: &'a OwnedMutexGuard<State>, udid: Option<String>) -> Result<SimDevice> {
    if let Some(udid) = udid {
        state.devices.iter().find(|d| d.udid == udid).cloned()
    } else {
        None
    }
    .ok_or_else(|| Error::Run("udid not found!!".to_string()))
}

serialize from string or vector

https://github.com/tami5/XcodeBase.nvim/blob/04ba82865312e6cea89dc19a030769b5f1f99a64/src/types/project.rs#L30

use super::Root;
#[cfg(feature = "daemon")]
use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;

/// Represent XcodeGen Project
#[derive(Debug, Deserialize, Serialize)]
pub struct Project {
    /// Project Name or rather xproj generated file name.
    pub name: String,
    /// The list of targets in the project mapped by name
    pub targets: HashMap<String, Target>,
    /// XcodeBase local configuration
    #[serde(rename(deserialize = "XcodeBase"), default)]
    pub xcode_base: LocalConfig,
    /// Root directory
    #[serde(skip)]
    pub root: Root,
    /// Connected Clients
    #[serde(default)]
    pub clients: Vec<i32>,
    /// Ignore Patterns
    #[serde(default)]
    pub ignore_patterns: Vec<String>,
}

/// Represent Xcode Target
// TODO: serialize from string or vector
#[derive(Debug, Deserialize, Serialize)]
pub struct Target {
    pub r#type: String,

Simplify rename file args by extracting current name from path.

https://github.com/tami5/XcodeBase.nvim/blob/8694c471ad68c99ff400c768f65ec17fa8fe9ead/src/daemon/command/rename_file.rs#L6

/// Rename file + class
#[derive(Debug)]
pub struct RenameFile {
    // TODO: Simplify rename file args by extracting current name from path.
    pub path: String,
    pub name: String,
    pub new_name: String,
}

// TODO: Implement file rename along with it's main class if any.
#[cfg(feature = "daemon")]
#[async_trait::async_trait]
impl crate::DaemonCommandExt for RenameFile {
    async fn handle(&self, _state: crate::SharedState) -> Result<()> {
        tracing::info!("Reanmed command");
        Ok(())
    }
}

impl TryFrom<Vec<&str>> for RenameFile {
    type Error = anyhow::Error;

    fn try_from(args: Vec<&str>) -> Result<Self, Self::Error> {
        let (path, new_name, name) = (args.get(0), args.get(1), args.get(2));
        if path.is_none() || name.is_none() || new_name.is_none() {
            anyhow::bail!(
                "Missing arugments: [path: {:?}, old_name: {:?}, name_name: {:?} ]",

[structure] rename or move to compile

for (pid, client) in state.clients.iter() {

if proc_exists(pid, || {}) {

let mut logger = client.new_logger("Compile Error", project_name, &None);

logger.set_running().await.ok();

let ref win = Some(logger.open_win().await.map_err(WatchError::r#continue)?);

logger.log(err.to_string(), win).await.ok();

logger.set_status_end(false, true).await.ok();

}

}

https://github.com/tami5/xbase.nvim/blob/a0531a3f0ebc4739cff8aa83b381bbcd06664291/src/watcher/handle/project.rs#L9

use std::{path::PathBuf, sync::Arc, time::Duration};
use tokio::sync::Mutex;

// TODO(structure): rename or move to compile
const START_MSG: &'static str = "⚙ auto compiling ..";

const SUCC_MESSAGE: &'static str = "✅ compiled";

pub async fn create(args: WatchArguments) -> Result<(), WatchError> {
    let WatchArguments {
        info, path, event, ..
    } = args;

    let info = info.lock_owned().await;
    let Client { root, .. } = info.try_into_project()?;

    if should_skip_compile(&event, &path, args.last_seen).await {
        tracing::trace!("Skipping {:#?}", &event);
        return Ok(());
    }

    tracing::trace!("[NewEvent] {:#?}", &event);

    let state = DAEMON_STATE.clone();
    let mut state = state.lock().await;
    let mut debounce = args.debounce.lock().await;

    let project_name = root.file_name().unwrap().to_string_lossy().to_string();

    echo_messsage_to_clients(&state, root, &project_name, START_MSG).await;

    {
        let res = try_updating_project_state(&mut state, &path, root, &project_name).await;
        if res.is_err() {
            *debounce = std::time::SystemTime::now();
            res?;
        }
    }

    ensure_server_configuration(&state, root, &project_name).await;

    if let Err(e) = generate_compiled_commands(&state, root, &project_name).await {
        *debounce = std::time::SystemTime::now();
        return Err(e);
    }

    echo_messsage_to_clients(&state, root, &project_name, SUCC_MESSAGE).await;

    *debounce = std::time::SystemTime::now();

    Ok(())
}

async fn try_updating_project_state<'a>(
    state: &mut tokio::sync::MutexGuard<'a, crate::state::State>,
    path: &PathBuf,
    root: &PathBuf,
    project_name: &String,
) -> Result<(), WatchError> {
    let generated = match crate::xcodegen::regenerate(path, &root).await {
        Ok(generated) => generated,
        Err(e) => {
            echo_error_to_clients(state, root, project_name, &e.to_string()).await;
            return Ok(());
        }
    };

    if generated {
        state
            .projects
            .get_mut(root)
            .ok_or_else(|| WatchError::Stop(format!("'{:?}' isn't registred project", root)))?
            .update()
            .await
            .map_err(WatchError::r#continue)?;
    }

    Ok(())
}

async fn ensure_server_configuration<'a>(
    state: &'a tokio::sync::MutexGuard<'a, crate::state::State>,
    root: &PathBuf,
    project_name: &String,
) {
    if compile::ensure_server_config(&root).await.is_err() {
        echo_error_to_clients(
            state,
            &root,
            project_name,
            "Fail to ensure build server configuration!",
        )
        .await;
    }
}

async fn generate_compiled_commands<'a>(
    state: &'a tokio::sync::MutexGuard<'a, crate::state::State>,
    root: &PathBuf,
    project_name: &String,
) -> Result<(), WatchError> {
    if let Err(err) = compile::update_compilation_file(&root).await {
        echo_error_to_clients(
            state,
            &root,
            project_name,
            "Fail to regenerate compilation database!",
        )
        .await;

        // use crate::util::proc_exists;
        // for (pid, client) in state.clients.iter() {
        //     if proc_exists(pid, || {}) {
        //         let mut logger = client.new_logger("Compile Error", project_name, &None);
        //         logger.set_running().await.ok();
        //         let ref win = Some(logger.open_win().await.map_err(WatchError::r#continue)?);
        //         logger.log(err.to_string(), win).await.ok();
        //         logger.set_status_end(false, true).await.ok();
        //     }
        // }
        return Err(WatchError::r#continue(err));
    }

    Ok(())
}

async fn echo_messsage_to_clients<'a>(
    state: &tokio::sync::MutexGuard<'a, crate::state::State>,
    root: &PathBuf,
    project_name: &String,
    msg: &str,
) {
    state.clients.echo_msg(&root, project_name, msg).await;
}

async fn echo_error_to_clients<'a>(
    state: &tokio::sync::MutexGuard<'a, crate::state::State>,
    root: &PathBuf,
    project_name: &String,
    msg: &str,
) {
    state.clients.echo_err(&root, project_name, msg).await;
}

async fn should_skip_compile(event: &Event, path: &PathBuf, last_seen: Arc<Mutex<String>>) -> bool {
    match &event.kind {
        EventKind::Create(_) => {

clear state when .compile get updated

Keep track of compile_filepath last modified state, and run state.clear() when it get changed

Might not be relevant since the whole server need to be restarted

[ComplieCommand] Support Objective-c files

https://github.com/tami5/XcodeBase.nvim/blob/a87a4ff04529327cc5d8f8093e91118478b6c44b/shared/src/xcode/compilation.rs#L11

// CREDIT: @SolaWing https://github.com/SolaWing/xcode-build-server/blob/master/xcode-build-server
// CREDIT: Richard Howell https://github.com/doc22940/sourcekit-lsp/blob/master/Tests/INPUTS/BuildServerBuildSystemTests.testBuildTargetOutputs/server.py

mod command;
use anyhow::Result;
use command::CompilationCommand;
use lazy_static::lazy_static;
use regex::Regex;
use std::collections::HashMap;

// TODO: Support compiling commands for objective-c files
// TODO: Test multiple module command compile
// TODO: support index store

pub struct Compiliation {
    pub commands: Vec<CompilationCommand>,
    lines: Vec<String>,
    clnum: usize,
    index_store_path: Vec<String>,
}

impl Compiliation {
    pub fn new(build_log: Vec<String>) -> Self {
        let mut parser = Self {
            lines: build_log,
            clnum: 0,
            commands: Vec::default(),
            index_store_path: Vec::default(),
        };

        for line in parser.lines.iter() {
            parser.clnum += 1;

            if line.starts_with("===") {
                continue;
            }

            if RE["swift_module"].is_match(line) {
                if let Some(command) = parser.swift_module_command() {
                    if let Some(isp) = &command.index_store_path {
                        parser.index_store_path.push(isp.clone())
                    }
                    parser.commands.push(command);
                    continue;
                };
            }
        }
        parser
    }

    /// Serialize to JSON string
    pub fn to_json(&self) -> Result<String, serde_json::Error> {
        serde_json::to_string_pretty(&self.commands)
    }
}

lazy_static! {
    static ref RE: HashMap<&'static str, Regex> = HashMap::from([
        (
            "swift_module",
            Regex::new(r"^CompileSwiftSources\s*").unwrap()
        ),
        (
            "swift",
            Regex::new(r"^CompileSwift\s+ \w+\s+ \w+\s+ (.+)$").unwrap()
        )
    ]);
}

impl Compiliation {
    /// Parse starting from current line as swift module
    /// Matching r"^CompileSwiftSources\s*"
    fn swift_module_command(&self) -> Option<CompilationCommand> {
        let directory = match self.lines.get(self.clnum) {
            Some(s) => s.trim().replace("cd ", ""),
            None => {
                tracing::error!("Found COMPILE_SWIFT_MODULE_PATERN but no more lines");
                return None;
            }
        };

        let command = match self.lines.get(self.clnum + 3) {
            Some(s) => s.trim().to_string(),
            None => {
                tracing::error!("Found COMPILE_SWIFT_MODULE_PATERN but couldn't extract command");
                return None;
            }
        };

        match CompilationCommand::new(directory, command) {
            Ok(command) => {
                tracing::debug!("Extracted {} Module Command", command.name);
                Some(command)
            }
            Err(e) => {
                tracing::error!("Fail to create swift module command {e}");
                None
            }
        }
    }

    /// Parse starting from current line as swift module
    /// Matching "^CompileSwift\s+ \w+\s+ \w+\s+ (.+)$"
    #[allow(dead_code)]
    fn swift_command(&self, _line: &str) {}
}

#[test]
fn test() {
    tokio::runtime::Runtime::new().unwrap().block_on(async {
        let build_log_test = tokio::fs::read_to_string("/Users/tami5/repos/swift/wordle/build.log")
            .await
            .unwrap()
            .split("\n")
            .map(|l| l.to_string())
            .collect();
        let compiliation = Compiliation::new(build_log_test);

        println!("{}", compiliation.to_json().unwrap())
    });
}

Test multiple module command compile

https://github.com/tami5/XcodeBase.nvim/blob/a87a4ff04529327cc5d8f8093e91118478b6c44b/shared/src/xcode/compilation.rs#L12

// CREDIT: @SolaWing https://github.com/SolaWing/xcode-build-server/blob/master/xcode-build-server
// CREDIT: Richard Howell https://github.com/doc22940/sourcekit-lsp/blob/master/Tests/INPUTS/BuildServerBuildSystemTests.testBuildTargetOutputs/server.py

mod command;
use anyhow::Result;
use command::CompilationCommand;
use lazy_static::lazy_static;
use regex::Regex;
use std::collections::HashMap;

// TODO: Support compiling commands for objective-c files
// TODO: Test multiple module command compile
// TODO: support index store

pub struct Compiliation {
    pub commands: Vec<CompilationCommand>,
    lines: Vec<String>,
    clnum: usize,
    index_store_path: Vec<String>,
}

impl Compiliation {
    pub fn new(build_log: Vec<String>) -> Self {
        let mut parser = Self {
            lines: build_log,
            clnum: 0,
            commands: Vec::default(),
            index_store_path: Vec::default(),
        };

        for line in parser.lines.iter() {
            parser.clnum += 1;

            if line.starts_with("===") {
                continue;
            }

            if RE["swift_module"].is_match(line) {
                if let Some(command) = parser.swift_module_command() {
                    if let Some(isp) = &command.index_store_path {
                        parser.index_store_path.push(isp.clone())
                    }
                    parser.commands.push(command);
                    continue;
                };
            }
        }
        parser
    }

    /// Serialize to JSON string
    pub fn to_json(&self) -> Result<String, serde_json::Error> {
        serde_json::to_string_pretty(&self.commands)
    }
}

lazy_static! {
    static ref RE: HashMap<&'static str, Regex> = HashMap::from([
        (
            "swift_module",
            Regex::new(r"^CompileSwiftSources\s*").unwrap()
        ),
        (
            "swift",
            Regex::new(r"^CompileSwift\s+ \w+\s+ \w+\s+ (.+)$").unwrap()
        )
    ]);
}

impl Compiliation {
    /// Parse starting from current line as swift module
    /// Matching r"^CompileSwiftSources\s*"
    fn swift_module_command(&self) -> Option<CompilationCommand> {
        let directory = match self.lines.get(self.clnum) {
            Some(s) => s.trim().replace("cd ", ""),
            None => {
                tracing::error!("Found COMPILE_SWIFT_MODULE_PATERN but no more lines");
                return None;
            }
        };

        let command = match self.lines.get(self.clnum + 3) {
            Some(s) => s.trim().to_string(),
            None => {
                tracing::error!("Found COMPILE_SWIFT_MODULE_PATERN but couldn't extract command");
                return None;
            }
        };

        match CompilationCommand::new(directory, command) {
            Ok(command) => {
                tracing::debug!("Extracted {} Module Command", command.name);
                Some(command)
            }
            Err(e) => {
                tracing::error!("Fail to create swift module command {e}");
                None
            }
        }
    }

    /// Parse starting from current line as swift module
    /// Matching "^CompileSwift\s+ \w+\s+ \w+\s+ (.+)$"
    #[allow(dead_code)]
    fn swift_command(&self, _line: &str) {}
}

#[test]
fn test() {
    tokio::runtime::Runtime::new().unwrap().block_on(async {
        let build_log_test = tokio::fs::read_to_string("/Users/tami5/repos/swift/wordle/build.log")
            .await
            .unwrap()
            .split("\n")
            .map(|l| l.to_string())
            .collect();
        let compiliation = Compiliation::new(build_log_test);

        println!("{}", compiliation.to_json().unwrap())
    });
}

Support overriding xcodebuild arguments

Not sure how important is this, but ideally I'd like to be able to add extra arguments for

when generating compiled commands, as well as doing actual builds and runs.

XcodeBase:

buildArguments: [];

compileArguments: [];

runArguments: [];

https://github.com/tami5/XcodeBase.nvim/blob/a87a4ff04529327cc5d8f8093e91118478b6c44b/shared/src/project.rs#L75

    pub fn targets(&self) -> &TargetMap {
        &self.targets
    }

    /// Build project with clean and return build log
    pub async fn fresh_build(&self) -> Result<Vec<String>> {
        /*
           TODO: Find away to get commands ran without doing xcodebuild clean

           Right now, in order to produce compiled commands and for `xcodebuild build` to spit out ran
           commands, we need to first run xcodebuild clean.

           NOTE: This far from possilbe after some research
        */
        xcode::clean(&self.root, &["-verbose"]).await?;

        /*
           TODO: Support overriding xcodebuild arguments

           Not sure how important is this, but ideally I'd like to be able to add extra arguments for
           when generating compiled commands, as well as doing actual builds and runs.

           ```yaml
           XcodeBase:
           buildArguments: [];
           compileArguments: [];
           runArguments: [];
           ```
        */
        xcode::build(&self.root, &["-verbose"]).await
    }
}

impl Project {}

check if macOS is the platform and run it

https://github.com/tami5/xbase.nvim/blob/a0531a3f0ebc4739cff8aa83b381bbcd06664291/src/daemon/requests/run.rs#L100

use super::*;
use crate::{
    nvim::BufferDirection,
    types::{BuildConfiguration, Platform},
};

#[cfg(feature = "daemon")]
use {crate::constants::DAEMON_STATE, crate::types::SimDevice, anyhow::anyhow as err};

#[derive(Debug, Serialize, Deserialize)]
pub struct DeviceLookup {
    name: String,
    udid: String,
    platform: Platform,
}

/// Run a project.
#[derive(Debug, Serialize, Deserialize)]
pub struct Run {
    pub client: Client,
    pub config: BuildConfiguration,
    pub device: Option<DeviceLookup>,
    pub direction: Option<BufferDirection>,
}

#[cfg(feature = "daemon")]
#[async_trait::async_trait]
impl Handler for Run {
    async fn handle(self) -> Result<()> {
        tracing::info!("⚙️ Running command: {:#?}", self);

        let Self {
            client,
            config,
            device,
            ..
        } = self;
        let Client { pid, root } = client;

        let direction = self.direction.clone();
        let state = DAEMON_STATE.clone().lock_owned().await;
        let platform = if let Some(d) = device.as_ref() {
            Some(d.platform.clone())
        } else {
            None
        };

        let nvim = state
            .clients
            .get(&pid)
            .ok_or_else(|| err!("no client found with {}", pid))?;

        let args = {
            let mut args = config.as_args();
            if let Some(platform) = platform {
                args.extend(platform.sdk_simulator_args())
            }
            args
        };

        let build_settings = xcodebuild::runner::build_settings(&root, &args).await?;
        let ref app_id = build_settings.product_bundle_identifier;

        // FIX(run): When running with release path_to_app is incorrect
        //
        // Err: application bundle was not found at the provided path.\nProvide a valid path to the desired application bundle.
        //
        // Path doesn't point to local directory build
        let ref path_to_app = build_settings.metal_library_output_dir;

        tracing::debug!("{app_id}: {:?}", path_to_app);
        let (success, ref win) = nvim
            .new_logger("Build", &config.target, &direction)
            .log_build_stream(&root, &args, false, true)
            .await?;

        if !success {
            let msg = format!("Failed: {} ", config.to_string());
            nvim.echo_err(&msg).await?;
            anyhow::bail!("{msg}");
        }

        let ref mut logger = nvim.new_logger("Run", &config.target, &direction);

        logger.set_running().await?;

        if let Some(mut device) = get_device(&state, device) {
            device.try_boot(logger, win).await?;
            device.try_install(path_to_app, app_id, logger, win).await?;
            device.try_launch(app_id, logger, win).await?;

            logger.set_status_end(true, true).await?;

            tokio::spawn(async move {
                let mut state = DAEMON_STATE.clone().lock_owned().await;
                state.devices.insert(device);
                state.sync_client_state().await
            });
        } else {
            // TODO: check if macOS is the platform and run it
        }

        Ok(())
    }
}

// let target = project.get_target(&config.target, ,)?;
#[cfg(feature = "daemon")]
fn get_device<'a>(
    state: &'a tokio::sync::OwnedMutexGuard<crate::state::State>,
    device: Option<DeviceLookup>,
) -> Option<SimDevice> {
    if let Some(device) = device {
        state
            .devices
            .iter()
            .find(|d| d.name == device.name && d.udid == device.udid)
            .cloned()
    } else {
        None
    }
}

#[cfg(feature = "lua")]
impl<'a> Requester<'a, Run> for Run {
    fn pre(lua: &Lua, msg: &Run) -> LuaResult<()> {
        lua.print(&msg.to_string());
        Ok(())
    }
}

impl ToString for Run {
    fn to_string(&self) -> String {
        if let Some(ref device) = self.device {
            format!("run [{}] with {}", device.name, self.config.to_string())
        } else {
            format!("run with {}", self.config.to_string())
        }
    }
}

#[cfg(feature = "lua")]
impl<'a> FromLua<'a> for Run {
    fn from_lua(lua_value: LuaValue<'a>, _lua: &'a Lua) -> LuaResult<Self> {
        let table = match lua_value {
            LuaValue::Table(t) => Ok(t),
            _ => Err(LuaError::external("Fail to deserialize Run")),
        }?;

        let device: Option<LuaTable> = table.get("device")?;

        Ok(Self {
            client: table.get("client")?,
            config: table.get("config")?,
            direction: table.get("direction").ok(),
            device: device
                .map(|d| {
                    let name = d.get("name").ok()?;
                    let udid = d.get("udid").ok()?;
                    let platform = d.get("platform").ok()?;
                    Some(DeviceLookup {
                        name,
                        udid,
                        platform,
                    })
                })
                .flatten(),
        })
    }
}

Implement build command

On neovim side:

  • Call the command after picking the target. If their is only a single target then just use that

  • This requires somehow given the client all information it needs in order present the user

with the options needed to build

https://github.com/tami5/XcodeBase.nvim/blob/8694c471ad68c99ff400c768f65ec17fa8fe9ead/src/daemon/command/build.rs#L12

    pub scheme: Option<String>,
}

// TODO: Implement build command
// On neovim side:
// - Call the command after picking the target. If their is only a single target then just use that
//  - This requires somehow given the client all information it needs in order present the user
//  with the options needed to build
#[cfg(feature = "daemon")]
#[async_trait::async_trait]
impl crate::DaemonCommandExt for Build {
    async fn handle(&self, _state: crate::state::SharedState) -> Result<()> {
        tracing::info!("build command");
        Ok(())
    }
}

impl TryFrom<Vec<&str>> for Build {
    type Error = anyhow::Error;

    fn try_from(_args: Vec<&str>) -> Result<Self, Self::Error> {
        Ok(Self {
            target: None,
            configuration: None,
            scheme: None,
        })
    }
}

impl Build {
    pub const KEY: &'static str = "build";

    pub fn request(target: &str, configuration: &str, scheme: &str) -> Result<()> {
        Daemon::execute(&["build", target, configuration, scheme])
    }
}

impl Build {
    #[cfg(feature = "lua")]
    pub fn lua(lua: &mlua::Lua, (t, c, s): (String, String, String)) -> mlua::Result<()> {
        use crate::LuaExtension;
        lua.trace(format!("Build (target: {t} configuration: {c}, scheme: {s})").as_ref())?;
        Self::request(&t, &c, &s).map_err(mlua::Error::external)
    }
}

Stop handle

https://github.com/tami5/XcodeBase.nvim/blob/a87a4ff04529327cc5d8f8093e91118478b6c44b/shared/src/watch.rs#L12

use crate::state::SharedState;
use crate::Command;
use notify::{Error, Event, RecommendedWatcher, RecursiveMode, Watcher};
use std::path::Path;
use std::result::Result;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::{mpsc, Mutex};
use tracing::{debug, trace};
use wax::{Glob, Pattern};

// TODO: Stop handle

pub async fn update(state: SharedState, _msg: Command) {
    let copy = state.clone();
    let mut current_state = copy.lock().await;
    let mut watched_roots: Vec<String> = vec![];
    let mut start_watching: Vec<String> = vec![];

    // TODO: Remove wathcers for workspaces that are no longer exist

    for key in current_state.watchers.keys() {
        watched_roots.push(key.clone());
    }

    for key in current_state.workspaces.keys() {
        if !watched_roots.contains(key) {
            start_watching.push(key.clone());
        }
    }

    for root in start_watching {
        let handle = new(state.clone(), root.clone());
        current_state.watchers.insert(root, handle);
    }
}

/// HACK: ignore seen paths.
///
/// Sometiems we get event for the same path, particularly
/// `ModifyKind::Name::Any` is ommited twice for the new path
/// and once for the old path.
///
/// This will compare last_seen with path, updates `last_seen` if not match,
/// else returns true.
async fn should_ignore(last_seen: Arc<Mutex<String>>, path: &str) -> bool {
    // HACK: Always return false for project.yml
    let path = path.to_string();
    if path.contains("project.yml") {
        return false;
    }
    let mut last_seen = last_seen.lock().await;
    if last_seen.to_string() == path {
        return true;
    } else {
        *last_seen = path;
        return false;
    }
}

// TODO: Cleanup get_ignore_patterns and decrease duplications
async fn get_ignore_patterns(state: SharedState, root: &String) -> Vec<String> {
    let mut patterns: Vec<String> = vec![
        "**/.git/**",
        "**/*.xcodeproj/**",
        "**/.*",
        "**/build/**",
        "**/buildServer.json",
    ]
    .iter()
    .map(|e| e.to_string())
    .collect();

    // FIXME: Addding extra ignore patterns to `ignore` local config requires restarting deamon.
    let extra_patterns = state
        .lock()
        .await
        .workspaces
        .get(root)
        .unwrap()
        .get_ignore_patterns();

    if let Some(extra_patterns) = extra_patterns {
        patterns.extend(extra_patterns);
    }

    patterns
}

fn new(state: SharedState, root: String) -> tokio::task::JoinHandle<anyhow::Result<()>> {
    // NOTE: should watch for registerd directories?
    // TODO: Support provideing additional ignore wildcard
    //
    // Some files can be generated as direct result of running build command.
    // In my case this `Info.plist`.
    //
    // For example,  define key inside project.yml under xcodebase key, ignoreGlob of type array.

    tokio::spawn(async move {
        let (tx, mut rx) = mpsc::channel(100);

        let mut watcher = RecommendedWatcher::new(move |res: Result<Event, Error>| {
            if res.is_ok() {
                tx.blocking_send(res.unwrap()).unwrap()
            };
        })?;

        watcher.watch(Path::new(&root), RecursiveMode::Recursive)?;
        watcher.configure(notify::Config::NoticeEvents(true))?;

        // HACK: ignore seen paths.
        let last_seen = Arc::new(Mutex::new(String::default()));

        // HACK: convert back to Vec<&str> for Glob to work.
        let patterns = get_ignore_patterns(state.clone(), &root).await;
        let patterns = patterns.iter().map(AsRef::as_ref).collect::<Vec<&str>>();
        let ignore = wax::any::<Glob, _>(patterns).unwrap();

        while let Some(event) = rx.recv().await {
            let state = state.clone();
            let path = match event.paths.get(0) {
                Some(p) => p.clone(),
                None => continue,
            };

            let path_string = match path.to_str() {
                Some(s) => s.to_string(),
                None => continue,
            };

            if ignore.is_match(&*path_string) {
                continue;
            }

            // debug!("[FSEVENT] {:?}", &event);
            // NOTE: maybe better handle in tokio::spawn?
            match &event.kind {
                notify::EventKind::Create(_) => {
                    tokio::time::sleep(Duration::new(1, 0)).await;
                    debug!("[FileCreated]: {:?}", path);
                }
                notify::EventKind::Remove(_) => {
                    tokio::time::sleep(Duration::new(1, 0)).await;
                    debug!("[FileRemoved]: {:?}", path);
                }
                notify::EventKind::Modify(m) => {
                    match m {
                        notify::event::ModifyKind::Data(e) => match e {
                            notify::event::DataChange::Content => {
                                if !path_string.contains("project.yml") {
                                    continue;
                                }
                                tokio::time::sleep(Duration::new(1, 0)).await;
                                debug!("[XcodeGenConfigUpdate]");
                                // HACK: Not sure why, but this is needed because xcodegen break.
                            }
                            _ => continue,
                        },
                        notify::event::ModifyKind::Name(_) => {
                            // HACK: only account for new path and skip duplications
                            if !Path::new(&path).exists()
                                || should_ignore(last_seen.clone(), &path_string).await
                            {
                                continue;
                            }
                            tokio::time::sleep(Duration::new(1, 0)).await;
                            debug!("[FileRenamed]: {:?}", path);
                        }
                        _ => continue,
                    }
                }
                _ => continue,
            }

            trace!("[NewEvent] {:#?}", &event);

            // let mut state = state.lock().await;

            match state.lock().await.workspaces.get_mut(&root) {
                Some(w) => {
                    w.on_dirctory_change(path, event.kind).await?;
                }
                // NOTE: should stop watch here
                None => continue,
            };
        }
        Ok(())
    })
}

support otherways to identify xcodegen project

Some would have xcodegen config as json file or

have different location to where they store xcodegen project config.

https://github.com/tami5/XcodeBase.nvim/blob/a87a4ff04529327cc5d8f8093e91118478b6c44b/shared/src/workspace.rs#L145

    pub fn get_target(&self, target_name: &str) -> Option<&Target> {
        self.project.targets().get(target_name)
    }

    /// Regenerate compiled commands and xcodeGen if project.yml exists
    pub async fn on_dirctory_change(&mut self, path: PathBuf, _event: EventKind) -> Result<()> {
        if self.is_xcodegen_project() {
            let is_config_file = path.file_name().unwrap().eq("project");
            self.update_xcodeproj(is_config_file).await?;
        }

        xcode::ensure_server_config_file(&self.root).await?;
        xcode::update_compiled_commands(&self.root, self.project.fresh_build().await?).await?;

        Ok(())
    }

    /// Update .compile commands
    pub async fn update_xcodeproj(&mut self, update_config: bool) -> Result<()> {
        /*
           FIXME: make xCodeGen binary path configurable.

           Current implementation will not work unless the user has xcodeGen located in
           `~/.mint/bin/xcodegen`. Should either make it configurable as well as support a
           number of paths by default.
        */
        let xcodegen_path = dirs::home_dir().unwrap().join(".mint/bin/xcodegen");
        let xcodegen = Command::new(xcodegen_path)
            .current_dir(self.root.clone())
            .stdout(Stdio::null())
            .arg("generate")
            .spawn()
            .expect("Failed to start xcodeGen.")
            .wait()
            .await
            .expect("Failed to run xcodeGen.");

        if xcodegen.success() {
            tracing::info!("Updated {}.xcodeproj", self.name());
            if update_config {
                tracing::debug!("Updated internal state.{}.project", self.name());
                let path = self.xcodegen_config_path();
                self.project = Project::new_from_project_yml(self.root.clone(), path).await?;
            }
        }

        Ok(())
    }

    /// Checks whether current workspace is xcodegen project.
    pub fn is_xcodegen_project(&self) -> bool {
        self.xcodegen_config_path().exists()
    }

    pub fn xcodegen_config_path(&self) -> PathBuf {
        /*
           TODO: support otherways to identify xcodegen project

           Some would have xcodegen config as json file or
           have different location to where they store xcodegen project config.
        */
        self.root.join("project.yml")
    }
    pub fn get_ignore_patterns(&self) -> Option<Vec<String>> {
        if self.is_xcodegen_project() {
            return Some(self.project.config().ignore.clone());
        }
        return None;
    }
}

[nvim] build log correct height

https://github.com/tami5/XcodeBase.nvim/blob/47e1f9b53214a45acf751ca55e4ac5a195637b6b/src/daemon/nvim.rs#L65

        mut stream: impl Stream<Item = String> + Unpin,
        clear: bool,
    ) -> Result<()> {
        let title = format!("[{title}] ------------------------------------------------------");
        let buf = Buffer::new(self.log_bufnr.into(), self.nvim.clone());

        if clear {
            buf.set_lines(0, -1, false, vec![]).await?;
        }

        let mut c = match buf.line_count().await? {
            1 => 0,
            count => count,
        };

        // TODO(nvim): build log correct height
        // TODO(nvim): make auto clear configurable
        let command = match self.get_window_direction(direction).await {
            Ok(open_command) => open_command,
            Err(e) => {

Check xcodebuild build output if it contains failure

Command succeed (return 0 status) but the output contains failure! need to be handled

somehow as errror

https://github.com/tami5/XcodeBase.nvim/blob/7533c48c396728bdbf7d0f0730741903b1e2c5fe/src/xcode/actions.rs#L36

        .map(|s| s.to_string())
        .collect();

    // TODO: Check xcodebuild build output if it contains failure
    //
    // Command succeed (return 0 status) but the output contains failure! need to be handled
    // somehow as errror
    tracing::trace!(
        "xcodebuild output: \n{:#?}\n\n\n---------------------------------- end",
        output

[nvim] make build config sysroot default to tmp in auto-build

https://github.com/tami5/XcodeBase.nvim/blob/47e1f9b53214a45acf751ca55e4ac5a195637b6b/src/types.rs#L47

#[cfg(feature = "lua")]
use mlua::prelude::*;
use serde::{Deserialize, Serialize};

#[derive(Debug, Default, Serialize, Deserialize, strum::Display)]
#[serde(untagged)]
pub enum XConfiguration {
    #[default]
    Debug,
    Release,
    Custom(String),
}

#[cfg(feature = "lua")]
impl<'a> FromLua<'a> for XConfiguration {
    fn from_lua(lua_value: LuaValue<'a>, _lua: &'a Lua) -> LuaResult<Self> {
        if let LuaValue::String(config) = lua_value {
            let value = config.to_str()?;
            Ok(match value {
                "debug" | "Debug" => Self::Debug,
                "release" | "Release" => Self::Release,
                _ => Self::Custom(value.to_string()),
            })
        } else if matches!(lua_value, LuaValue::Nil) {
            Ok(Self::Debug)
        } else {
            Err(LuaError::external("Expected a table got XConfiguration"))
        }
    }
}

/// Xcode Scheme
///
/// An Xcode scheme defines a collection of targets to build, a configuration to use when building,
/// and a collection of tests to execute.
pub type XScheme = String;

/// Xcode Target
///
/// A target specifies a product to build and contains the instructions for building the product
/// from a set of files in a project or workspace.
pub type XTarget = String;

/// Fields required to build a project
#[derive(Default, Debug, Serialize, Deserialize)]
pub struct BuildConfiguration {
    /// TODO(nvim): make build config sysroot default to tmp in auto-build
    pub sysroot: Option<String>,
    /// Target to build
    pub target: Option<XTarget>,
    /// Configuration to build with, default Debug
    #[serde(default)]
    pub configuration: XConfiguration,
    /// Scheme to build with
    pub scheme: Option<XScheme>,
}
impl std::fmt::Display for BuildConfiguration {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "xcodebuild")?;
        write!(f, " -configuration {}", self.configuration)?;

        if let Some(ref sysroot) = self.sysroot {
            write!(f, " -sysroot {sysroot}")?;
        }
        if let Some(ref scheme) = self.scheme {
            write!(f, " -scheme {scheme}")?;
        }
        if let Some(ref target) = self.target {
            write!(f, " -target {target}")?;
        }
        Ok(())
    }
}

#[cfg(feature = "lua")]
impl<'a> FromLua<'a> for BuildConfiguration {
    fn from_lua(lua_value: LuaValue<'a>, _lua: &'a Lua) -> LuaResult<Self> {
        if let LuaValue::Table(table) = lua_value {
            Ok(Self {
                sysroot: table.get("sysroot")?,
                target: table.get("target")?,
                configuration: table.get("configuration")?,
                scheme: table.get("scheme")?,
            })
        } else {
            Ok(BuildConfiguration::default())
        }
    }
}

[nvim] build log control what direction to open buffer

https://github.com/tami5/XcodeBase.nvim/blob/a9eede75989425943de53943e00fc1faa23f6d97/src/daemon/nvim.rs#L82

    pub async fn log_warn(&self, scope: &str, msg: impl ToString) -> Result<()> {
        self.log("warn", scope, msg).await
    }

    pub async fn log_to_buffer(
        &self,
        title: &str,
        direction: WindowType,
        mut stream: impl tokio_stream::Stream<Item = String> + Unpin,
        clear: bool,
    ) -> Result<()> {
        let title = format!("[ {title} ]: ----> ");
        let buffer = Buffer::new(self.log_bufnr.into(), self.nvim.clone());

        if clear {
            buffer.set_lines(0, -1, false, vec![]).await?;
        }

        let mut c = match buffer.line_count().await? {
            1 => 0,
            count => count,
        };

        // TODO(nvim): build log control what direction to open buffer
        // TODO(nvim): build log correct width
        // TODO(nvim): build log auto scroll
        let command = match direction {
            // TOOD: build log float
            WindowType::Float => format!("sbuffer {}", self.log_bufnr),
            WindowType::Vertical => format!("vert sbuffer {}", self.log_bufnr),
            WindowType::Horizontal => format!("sbuffer {}", self.log_bufnr),
        };

        self.exec(&command, false).await?;

        buffer.set_lines(c, c + 1, false, vec![title]).await?;
        c += 1;

        while let Some(line) = stream.next().await {
            buffer.set_lines(c, c + 1, false, vec![line]).await?;
            c += 1
        }

        Ok(())
    }
}

impl std::fmt::Debug for Nvim {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_tuple("Nvim").finish()
    }
}

impl ops::Deref for Nvim {
    type Target = Neovim<Compat<WriteHalf<Connection>>>;
    fn deref(&self) -> &Self::Target {
        &self.nvim
    }
}

Remove wathcers for workspaces that are no longer exist

/

/ Sometiems we get event for the same path, particularly

/ ModifyKind::Name::Any is ommited twice for the new path

/ and once for the old path.

/

/ This will compare last_seen with path, updates last_seen if not match,

/ else returns true.

https://github.com/tami5/XcodeBase.nvim/blob/a87a4ff04529327cc5d8f8093e91118478b6c44b/shared/src/watch.rs#L20

use crate::state::SharedState;
use crate::Command;
use notify::{Error, Event, RecommendedWatcher, RecursiveMode, Watcher};
use std::path::Path;
use std::result::Result;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::{mpsc, Mutex};
use tracing::{debug, trace};
use wax::{Glob, Pattern};

// TODO: Stop handle

pub async fn update(state: SharedState, _msg: Command) {
    let copy = state.clone();
    let mut current_state = copy.lock().await;
    let mut watched_roots: Vec<String> = vec![];
    let mut start_watching: Vec<String> = vec![];

    // TODO: Remove wathcers for workspaces that are no longer exist

    for key in current_state.watchers.keys() {
        watched_roots.push(key.clone());
    }

    for key in current_state.workspaces.keys() {
        if !watched_roots.contains(key) {
            start_watching.push(key.clone());
        }
    }

    for root in start_watching {
        let handle = new(state.clone(), root.clone());
        current_state.watchers.insert(root, handle);
    }
}

/// HACK: ignore seen paths.
///
/// Sometiems we get event for the same path, particularly
/// `ModifyKind::Name::Any` is ommited twice for the new path
/// and once for the old path.
///
/// This will compare last_seen with path, updates `last_seen` if not match,
/// else returns true.
async fn should_ignore(last_seen: Arc<Mutex<String>>, path: &str) -> bool {
    // HACK: Always return false for project.yml
    let path = path.to_string();
    if path.contains("project.yml") {
        return false;
    }
    let mut last_seen = last_seen.lock().await;
    if last_seen.to_string() == path {
        return true;
    } else {
        *last_seen = path;
        return false;
    }
}

// TODO: Cleanup get_ignore_patterns and decrease duplications
async fn get_ignore_patterns(state: SharedState, root: &String) -> Vec<String> {
    let mut patterns: Vec<String> = vec![
        "**/.git/**",
        "**/*.xcodeproj/**",
        "**/.*",
        "**/build/**",
        "**/buildServer.json",
    ]
    .iter()
    .map(|e| e.to_string())
    .collect();

    // FIXME: Addding extra ignore patterns to `ignore` local config requires restarting deamon.
    let extra_patterns = state
        .lock()
        .await
        .workspaces
        .get(root)
        .unwrap()
        .get_ignore_patterns();

    if let Some(extra_patterns) = extra_patterns {
        patterns.extend(extra_patterns);
    }

    patterns
}

fn new(state: SharedState, root: String) -> tokio::task::JoinHandle<anyhow::Result<()>> {
    // NOTE: should watch for registerd directories?
    // TODO: Support provideing additional ignore wildcard
    //
    // Some files can be generated as direct result of running build command.
    // In my case this `Info.plist`.
    //
    // For example,  define key inside project.yml under xcodebase key, ignoreGlob of type array.

    tokio::spawn(async move {
        let (tx, mut rx) = mpsc::channel(100);

        let mut watcher = RecommendedWatcher::new(move |res: Result<Event, Error>| {
            if res.is_ok() {
                tx.blocking_send(res.unwrap()).unwrap()
            };
        })?;

        watcher.watch(Path::new(&root), RecursiveMode::Recursive)?;
        watcher.configure(notify::Config::NoticeEvents(true))?;

        // HACK: ignore seen paths.
        let last_seen = Arc::new(Mutex::new(String::default()));

        // HACK: convert back to Vec<&str> for Glob to work.
        let patterns = get_ignore_patterns(state.clone(), &root).await;
        let patterns = patterns.iter().map(AsRef::as_ref).collect::<Vec<&str>>();
        let ignore = wax::any::<Glob, _>(patterns).unwrap();

        while let Some(event) = rx.recv().await {
            let state = state.clone();
            let path = match event.paths.get(0) {
                Some(p) => p.clone(),
                None => continue,
            };

            let path_string = match path.to_str() {
                Some(s) => s.to_string(),
                None => continue,
            };

            if ignore.is_match(&*path_string) {
                continue;
            }

            // debug!("[FSEVENT] {:?}", &event);
            // NOTE: maybe better handle in tokio::spawn?
            match &event.kind {
                notify::EventKind::Create(_) => {
                    tokio::time::sleep(Duration::new(1, 0)).await;
                    debug!("[FileCreated]: {:?}", path);
                }
                notify::EventKind::Remove(_) => {
                    tokio::time::sleep(Duration::new(1, 0)).await;
                    debug!("[FileRemoved]: {:?}", path);
                }
                notify::EventKind::Modify(m) => {
                    match m {
                        notify::event::ModifyKind::Data(e) => match e {
                            notify::event::DataChange::Content => {
                                if !path_string.contains("project.yml") {
                                    continue;
                                }
                                tokio::time::sleep(Duration::new(1, 0)).await;
                                debug!("[XcodeGenConfigUpdate]");
                                // HACK: Not sure why, but this is needed because xcodegen break.
                            }
                            _ => continue,
                        },
                        notify::event::ModifyKind::Name(_) => {
                            // HACK: only account for new path and skip duplications
                            if !Path::new(&path).exists()
                                || should_ignore(last_seen.clone(), &path_string).await
                            {
                                continue;
                            }
                            tokio::time::sleep(Duration::new(1, 0)).await;
                            debug!("[FileRenamed]: {:?}", path);
                        }
                        _ => continue,
                    }
                }
                _ => continue,
            }

            trace!("[NewEvent] {:#?}", &event);

            // let mut state = state.lock().await;

            match state.lock().await.workspaces.get_mut(&root) {
                Some(w) => {
                    w.on_dirctory_change(path, event.kind).await?;
                }
                // NOTE: should stop watch here
                None => continue,
            };
        }
        Ok(())
    })
}

[nvim] close log buffer if it is open for new direction

Currently the buffer direction will be ignored if the buffer is opened already

https://github.com/tami5/xbase.nvim/blob/b7a947a97abca5c4ac33abe1d9407e0d23a40c1f/src/xcode.rs#L72

    */
    append_build_root(&root, vec!["clean".into(), "build".into()])?
        .pipe(|args| spawn(root, args))
        .await?
        .pipe(Ok)
}
pub async fn build_with_loggger<'a, P: AsRef<Path>>(
    logger: &mut Logger<'a>,
    root: P,
    args: &Vec<String>,
    clear: bool,
    open: bool,
) -> Result<bool> {
    let mut stream = crate::xcode::stream_build(root, args).await?;

    // TODO(nvim): close log buffer if it is open for new direction
    //
    // Currently the buffer direction will be ignored if the buffer is opened already

    if clear {
        logger.clear_content().await?;
    }

    // TODO(nvim): build log correct height
    if open {
        logger.open_win().await?;
    }

    let mut success = false;

    logger.set_running().await?;
    logger.log_title().await?;

    while let Some(line) = stream.next().await {
        line.contains("Succeed").then(|| success = true);

        logger.log(line).await?;
    }

    logger.set_status_end(success, true).await?;

    Ok(success)
}

pub fn append_build_root<P: AsRef<Path> + std::fmt::Debug>(

provide mapping to close runners.

If there is more then one runner then pick, else close from log current buffer.

C-c in normal/insert mode should close that process

// TODO(nvim): provide mapping to close runners.

            return Err(Error::Build(msg));
        }

        // TODO(daemon): insert handler to state.runners
        // TODO(nvim): provide mapping to close runners.
        //
        // If there is more then one runner then pick, else close from current buffer.
        // C-c in normal/insert mode should close that process
        Runner {
            target: self.config.target,
            platform,

support watching multiple targets at the same time

One way is to change watch to watchers of (maybe hash set of WatchStart)

Also to make it convinent, the hash set should contain pid as first item in union.

https://github.com/tami5/XcodeBase.nvim/blob/b8b74a1c9dcf3be675140742b5393f19bfe13262/src/daemon/state/workspace.rs#L34

    #[cfg(feature = "daemon")]
    pub clients: HashMap<i32, Nvim>,
    #[cfg(feature = "daemon")]
    // TODO: support watching multiple targets at the same time
    //
    // One way is to change watch to watchers of (maybe hash set of WatchStart)
    //
    // Also to make it convinent, the hash set should contain pid as first item in union.
    pub watch: Option<(crate::daemon::WatchStart, JoinHandle<Result<()>>)>,
}

Cleanup get_ignore_patterns and decrease duplications

https://github.com/tami5/XcodeBase.nvim/blob/a87a4ff04529327cc5d8f8093e91118478b6c44b/shared/src/watch.rs#L61

use crate::state::SharedState;
use crate::Command;
use notify::{Error, Event, RecommendedWatcher, RecursiveMode, Watcher};
use std::path::Path;
use std::result::Result;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::{mpsc, Mutex};
use tracing::{debug, trace};
use wax::{Glob, Pattern};

// TODO: Stop handle

pub async fn update(state: SharedState, _msg: Command) {
    let copy = state.clone();
    let mut current_state = copy.lock().await;
    let mut watched_roots: Vec<String> = vec![];
    let mut start_watching: Vec<String> = vec![];

    // TODO: Remove wathcers for workspaces that are no longer exist

    for key in current_state.watchers.keys() {
        watched_roots.push(key.clone());
    }

    for key in current_state.workspaces.keys() {
        if !watched_roots.contains(key) {
            start_watching.push(key.clone());
        }
    }

    for root in start_watching {
        let handle = new(state.clone(), root.clone());
        current_state.watchers.insert(root, handle);
    }
}

/// HACK: ignore seen paths.
///
/// Sometiems we get event for the same path, particularly
/// `ModifyKind::Name::Any` is ommited twice for the new path
/// and once for the old path.
///
/// This will compare last_seen with path, updates `last_seen` if not match,
/// else returns true.
async fn should_ignore(last_seen: Arc<Mutex<String>>, path: &str) -> bool {
    // HACK: Always return false for project.yml
    let path = path.to_string();
    if path.contains("project.yml") {
        return false;
    }
    let mut last_seen = last_seen.lock().await;
    if last_seen.to_string() == path {
        return true;
    } else {
        *last_seen = path;
        return false;
    }
}

// TODO: Cleanup get_ignore_patterns and decrease duplications
async fn get_ignore_patterns(state: SharedState, root: &String) -> Vec<String> {
    let mut patterns: Vec<String> = vec![
        "**/.git/**",
        "**/*.xcodeproj/**",
        "**/.*",
        "**/build/**",
        "**/buildServer.json",
    ]
    .iter()
    .map(|e| e.to_string())
    .collect();

    // FIXME: Addding extra ignore patterns to `ignore` local config requires restarting deamon.
    let extra_patterns = state
        .lock()
        .await
        .workspaces
        .get(root)
        .unwrap()
        .get_ignore_patterns();

    if let Some(extra_patterns) = extra_patterns {
        patterns.extend(extra_patterns);
    }

    patterns
}

fn new(state: SharedState, root: String) -> tokio::task::JoinHandle<anyhow::Result<()>> {
    // NOTE: should watch for registerd directories?
    // TODO: Support provideing additional ignore wildcard
    //
    // Some files can be generated as direct result of running build command.
    // In my case this `Info.plist`.
    //
    // For example,  define key inside project.yml under xcodebase key, ignoreGlob of type array.

    tokio::spawn(async move {
        let (tx, mut rx) = mpsc::channel(100);

        let mut watcher = RecommendedWatcher::new(move |res: Result<Event, Error>| {
            if res.is_ok() {
                tx.blocking_send(res.unwrap()).unwrap()
            };
        })?;

        watcher.watch(Path::new(&root), RecursiveMode::Recursive)?;
        watcher.configure(notify::Config::NoticeEvents(true))?;

        // HACK: ignore seen paths.
        let last_seen = Arc::new(Mutex::new(String::default()));

        // HACK: convert back to Vec<&str> for Glob to work.
        let patterns = get_ignore_patterns(state.clone(), &root).await;
        let patterns = patterns.iter().map(AsRef::as_ref).collect::<Vec<&str>>();
        let ignore = wax::any::<Glob, _>(patterns).unwrap();

        while let Some(event) = rx.recv().await {
            let state = state.clone();
            let path = match event.paths.get(0) {
                Some(p) => p.clone(),
                None => continue,
            };

            let path_string = match path.to_str() {
                Some(s) => s.to_string(),
                None => continue,
            };

            if ignore.is_match(&*path_string) {
                continue;
            }

            // debug!("[FSEVENT] {:?}", &event);
            // NOTE: maybe better handle in tokio::spawn?
            match &event.kind {
                notify::EventKind::Create(_) => {
                    tokio::time::sleep(Duration::new(1, 0)).await;
                    debug!("[FileCreated]: {:?}", path);
                }
                notify::EventKind::Remove(_) => {
                    tokio::time::sleep(Duration::new(1, 0)).await;
                    debug!("[FileRemoved]: {:?}", path);
                }
                notify::EventKind::Modify(m) => {
                    match m {
                        notify::event::ModifyKind::Data(e) => match e {
                            notify::event::DataChange::Content => {
                                if !path_string.contains("project.yml") {
                                    continue;
                                }
                                tokio::time::sleep(Duration::new(1, 0)).await;
                                debug!("[XcodeGenConfigUpdate]");
                                // HACK: Not sure why, but this is needed because xcodegen break.
                            }
                            _ => continue,
                        },
                        notify::event::ModifyKind::Name(_) => {
                            // HACK: only account for new path and skip duplications
                            if !Path::new(&path).exists()
                                || should_ignore(last_seen.clone(), &path_string).await
                            {
                                continue;
                            }
                            tokio::time::sleep(Duration::new(1, 0)).await;
                            debug!("[FileRenamed]: {:?}", path);
                        }
                        _ => continue,
                    }
                }
                _ => continue,
            }

            trace!("[NewEvent] {:#?}", &event);

            // let mut state = state.lock().await;

            match state.lock().await.workspaces.get_mut(&root) {
                Some(w) => {
                    w.on_dirctory_change(path, event.kind).await?;
                }
                // NOTE: should stop watch here
                None => continue,
            };
        }
        Ok(())
    })
}

[nvim] make auto clear configurable

https://github.com/tami5/XcodeBase.nvim/blob/47e1f9b53214a45acf751ca55e4ac5a195637b6b/src/daemon/nvim.rs#L66

        mut stream: impl Stream<Item = String> + Unpin,
        clear: bool,
    ) -> Result<()> {
        let title = format!("[{title}] ------------------------------------------------------");
        let buf = Buffer::new(self.log_bufnr.into(), self.nvim.clone());

        if clear {
            buf.set_lines(0, -1, false, vec![]).await?;
        }

        let mut c = match buf.line_count().await? {
            1 => 0,
            count => count,
        };

        // TODO(nvim): build log correct height
        // TODO(nvim): make auto clear configurable
        let command = match self.get_window_direction(direction).await {
            Ok(open_command) => open_command,
            Err(e) => {

support custom index store

/ Matching r"^CompileSwiftSources\s*"

/ Matching "^CompileSwift\s+ \w+\s+ \w+\s+ (.+)$"

https://github.com/tami5/XcodeBase.nvim/blob/a87a4ff04529327cc5d8f8093e91118478b6c44b/shared/src/xcode/compilation.rs#L13

// CREDIT: @SolaWing https://github.com/SolaWing/xcode-build-server/blob/master/xcode-build-server
// CREDIT: Richard Howell https://github.com/doc22940/sourcekit-lsp/blob/master/Tests/INPUTS/BuildServerBuildSystemTests.testBuildTargetOutputs/server.py

mod command;
use anyhow::Result;
use command::CompilationCommand;
use lazy_static::lazy_static;
use regex::Regex;
use std::collections::HashMap;

// TODO: Support compiling commands for objective-c files
// TODO: Test multiple module command compile
// TODO: support index store

pub struct Compiliation {
    pub commands: Vec<CompilationCommand>,
    lines: Vec<String>,
    clnum: usize,
    index_store_path: Vec<String>,
}

impl Compiliation {
    pub fn new(build_log: Vec<String>) -> Self {
        let mut parser = Self {
            lines: build_log,
            clnum: 0,
            commands: Vec::default(),
            index_store_path: Vec::default(),
        };

        for line in parser.lines.iter() {
            parser.clnum += 1;

            if line.starts_with("===") {
                continue;
            }

            if RE["swift_module"].is_match(line) {
                if let Some(command) = parser.swift_module_command() {
                    if let Some(isp) = &command.index_store_path {
                        parser.index_store_path.push(isp.clone())
                    }
                    parser.commands.push(command);
                    continue;
                };
            }
        }
        parser
    }

    /// Serialize to JSON string
    pub fn to_json(&self) -> Result<String, serde_json::Error> {
        serde_json::to_string_pretty(&self.commands)
    }
}

lazy_static! {
    static ref RE: HashMap<&'static str, Regex> = HashMap::from([
        (
            "swift_module",
            Regex::new(r"^CompileSwiftSources\s*").unwrap()
        ),
        (
            "swift",
            Regex::new(r"^CompileSwift\s+ \w+\s+ \w+\s+ (.+)$").unwrap()
        )
    ]);
}

impl Compiliation {
    /// Parse starting from current line as swift module
    /// Matching r"^CompileSwiftSources\s*"
    fn swift_module_command(&self) -> Option<CompilationCommand> {
        let directory = match self.lines.get(self.clnum) {
            Some(s) => s.trim().replace("cd ", ""),
            None => {
                tracing::error!("Found COMPILE_SWIFT_MODULE_PATERN but no more lines");
                return None;
            }
        };

        let command = match self.lines.get(self.clnum + 3) {
            Some(s) => s.trim().to_string(),
            None => {
                tracing::error!("Found COMPILE_SWIFT_MODULE_PATERN but couldn't extract command");
                return None;
            }
        };

        match CompilationCommand::new(directory, command) {
            Ok(command) => {
                tracing::debug!("Extracted {} Module Command", command.name);
                Some(command)
            }
            Err(e) => {
                tracing::error!("Fail to create swift module command {e}");
                None
            }
        }
    }

    /// Parse starting from current line as swift module
    /// Matching "^CompileSwift\s+ \w+\s+ \w+\s+ (.+)$"
    #[allow(dead_code)]
    fn swift_command(&self, _line: &str) {}
}

#[test]
fn test() {
    tokio::runtime::Runtime::new().unwrap().block_on(async {
        let build_log_test = tokio::fs::read_to_string("/Users/tami5/repos/swift/wordle/build.log")
            .await
            .unwrap()
            .split("\n")
            .map(|l| l.to_string())
            .collect();
        let compiliation = Compiliation::new(build_log_test);

        println!("{}", compiliation.to_json().unwrap())
    });
}

Support provideing additional ignore wildcard

Some files can be generated as direct result of running build command.

In my case this Info.plist.

For example, define key inside project.yml under xcodebase key, ignoreGlob of type array.

NOTE: maybe better handle in tokio::spawn?

https://github.com/tami5/XcodeBase.nvim/blob/a87a4ff04529327cc5d8f8093e91118478b6c44b/shared/src/watch.rs#L92

use crate::state::SharedState;
use crate::Command;
use notify::{Error, Event, RecommendedWatcher, RecursiveMode, Watcher};
use std::path::Path;
use std::result::Result;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::{mpsc, Mutex};
use tracing::{debug, trace};
use wax::{Glob, Pattern};

// TODO: Stop handle

pub async fn update(state: SharedState, _msg: Command) {
    let copy = state.clone();
    let mut current_state = copy.lock().await;
    let mut watched_roots: Vec<String> = vec![];
    let mut start_watching: Vec<String> = vec![];

    // TODO: Remove wathcers for workspaces that are no longer exist

    for key in current_state.watchers.keys() {
        watched_roots.push(key.clone());
    }

    for key in current_state.workspaces.keys() {
        if !watched_roots.contains(key) {
            start_watching.push(key.clone());
        }
    }

    for root in start_watching {
        let handle = new(state.clone(), root.clone());
        current_state.watchers.insert(root, handle);
    }
}

/// HACK: ignore seen paths.
///
/// Sometiems we get event for the same path, particularly
/// `ModifyKind::Name::Any` is ommited twice for the new path
/// and once for the old path.
///
/// This will compare last_seen with path, updates `last_seen` if not match,
/// else returns true.
async fn should_ignore(last_seen: Arc<Mutex<String>>, path: &str) -> bool {
    // HACK: Always return false for project.yml
    let path = path.to_string();
    if path.contains("project.yml") {
        return false;
    }
    let mut last_seen = last_seen.lock().await;
    if last_seen.to_string() == path {
        return true;
    } else {
        *last_seen = path;
        return false;
    }
}

// TODO: Cleanup get_ignore_patterns and decrease duplications
async fn get_ignore_patterns(state: SharedState, root: &String) -> Vec<String> {
    let mut patterns: Vec<String> = vec![
        "**/.git/**",
        "**/*.xcodeproj/**",
        "**/.*",
        "**/build/**",
        "**/buildServer.json",
    ]
    .iter()
    .map(|e| e.to_string())
    .collect();

    // FIXME: Addding extra ignore patterns to `ignore` local config requires restarting deamon.
    let extra_patterns = state
        .lock()
        .await
        .workspaces
        .get(root)
        .unwrap()
        .get_ignore_patterns();

    if let Some(extra_patterns) = extra_patterns {
        patterns.extend(extra_patterns);
    }

    patterns
}

fn new(state: SharedState, root: String) -> tokio::task::JoinHandle<anyhow::Result<()>> {
    // NOTE: should watch for registerd directories?
    // TODO: Support provideing additional ignore wildcard
    //
    // Some files can be generated as direct result of running build command.
    // In my case this `Info.plist`.
    //
    // For example,  define key inside project.yml under xcodebase key, ignoreGlob of type array.

    tokio::spawn(async move {
        let (tx, mut rx) = mpsc::channel(100);

        let mut watcher = RecommendedWatcher::new(move |res: Result<Event, Error>| {
            if res.is_ok() {
                tx.blocking_send(res.unwrap()).unwrap()
            };
        })?;

        watcher.watch(Path::new(&root), RecursiveMode::Recursive)?;
        watcher.configure(notify::Config::NoticeEvents(true))?;

        // HACK: ignore seen paths.
        let last_seen = Arc::new(Mutex::new(String::default()));

        // HACK: convert back to Vec<&str> for Glob to work.
        let patterns = get_ignore_patterns(state.clone(), &root).await;
        let patterns = patterns.iter().map(AsRef::as_ref).collect::<Vec<&str>>();
        let ignore = wax::any::<Glob, _>(patterns).unwrap();

        while let Some(event) = rx.recv().await {
            let state = state.clone();
            let path = match event.paths.get(0) {
                Some(p) => p.clone(),
                None => continue,
            };

            let path_string = match path.to_str() {
                Some(s) => s.to_string(),
                None => continue,
            };

            if ignore.is_match(&*path_string) {
                continue;
            }

            // debug!("[FSEVENT] {:?}", &event);
            // NOTE: maybe better handle in tokio::spawn?
            match &event.kind {
                notify::EventKind::Create(_) => {
                    tokio::time::sleep(Duration::new(1, 0)).await;
                    debug!("[FileCreated]: {:?}", path);
                }
                notify::EventKind::Remove(_) => {
                    tokio::time::sleep(Duration::new(1, 0)).await;
                    debug!("[FileRemoved]: {:?}", path);
                }
                notify::EventKind::Modify(m) => {
                    match m {
                        notify::event::ModifyKind::Data(e) => match e {
                            notify::event::DataChange::Content => {
                                if !path_string.contains("project.yml") {
                                    continue;
                                }
                                tokio::time::sleep(Duration::new(1, 0)).await;
                                debug!("[XcodeGenConfigUpdate]");
                                // HACK: Not sure why, but this is needed because xcodegen break.
                            }
                            _ => continue,
                        },
                        notify::event::ModifyKind::Name(_) => {
                            // HACK: only account for new path and skip duplications
                            if !Path::new(&path).exists()
                                || should_ignore(last_seen.clone(), &path_string).await
                            {
                                continue;
                            }
                            tokio::time::sleep(Duration::new(1, 0)).await;
                            debug!("[FileRenamed]: {:?}", path);
                        }
                        _ => continue,
                    }
                }
                _ => continue,
            }

            trace!("[NewEvent] {:#?}", &event);

            // let mut state = state.lock().await;

            match state.lock().await.workspaces.get_mut(&root) {
                Some(w) => {
                    w.on_dirctory_change(path, event.kind).await?;
                }
                // NOTE: should stop watch here
                None => continue,
            };
        }
        Ok(())
    })
}

ensure .compile file on on_directory_change and in workspace initialization

https://github.com/tami5/XcodeBase.nvim/blob/d5226d09d5f90de0186939ba058c1f236451bd43/src/daemon/state/workspace.rs#L111

        path: PathBuf,
        _event: &notify::EventKind,
    ) -> Result<()> {
        use tap::Pipe;

        if crate::xcodegen::is_workspace(self) {
            self.update_xcodeproj(
                path.file_name()
                    .ok_or_else(|| anyhow::anyhow!("Fail to get filename from {:?}", path))?
                    .eq("project.yml"),
            )
            .await?;
        }

        #[cfg(feature = "xcode")]
        crate::xcode::ensure_server_config_file(&self.root).await?;

        // TODO: ensure .compile file on on_directory_change and in workspace initialization

        #[cfg(feature = "compilation")]
        self.project
            .fresh_build()
            .await?
            .pipe(crate::compile::CompilationDatabase::from_logs)
            .pipe(|cmd| serde_json::to_vec_pretty(&cmd.0))?
            .pipe(|json| tokio::fs::write(self.root.join(".compile"), json))
            .await

[daemon] update clients state account only for clients with specific project.

NOTE: Update all clients state

https://github.com/tami5/XcodeBase.nvim/blob/04ba82865312e6cea89dc19a030769b5f1f99a64/src/daemon/requests/watch_target.rs#L62

use super::*;
use crate::types::BuildConfiguration;
use std::fmt::Debug;

#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum WatchKind {
    Stop,
    Start,
}

/// Stop Watching a project.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct WatchTarget {
    pub client: Client,
    pub config: BuildConfiguration,
    pub ops: WatchKind,
}

#[cfg(feature = "daemon")]
#[async_trait]
impl Handler for WatchTarget {
    async fn handle(self) -> Result<()> {
        use crate::constants::DAEMON_STATE;

        let Self {
            client,
            config,
            ops,
        } = &self;

        let BuildConfiguration { target, .. } = config;

        let state = DAEMON_STATE.clone();
        let mut state = state.lock().await;

        if target.is_empty() {
            anyhow::bail!("No target specified!")
        }

        match ops {
            WatchKind::Start => {
                // NOTE: Get project associate ignore pattern
                let ignore_patterns = state
                    .projects
                    .get_mut(&client.root)
                    .ok_or_else(|| anyhow::anyhow!("No project for {:#?}", config))?
                    .ignore_patterns
                    .clone();

                // NOTE: add new target watcher
                state
                    .watcher
                    .add_target_watcher(&self, ignore_patterns)
                    .await;
            }
            WatchKind::Stop => {
                // NOTE: Remove target watcher
                state.watcher.remove_target_watcher(&self, client).await;
            }
        }

        // TODO(daemon): update clients state account only for clients with specific project.
        // NOTE: Update all clients state
        state.sync_client_state().await?;

        Ok(())
    }
}

#[cfg(feature = "lua")]
impl<'a> Requester<'a, WatchTarget> for WatchTarget {
    fn pre(lua: &Lua, msg: &WatchTarget) -> LuaResult<()> {
        match msg.ops {
            WatchKind::Start => {
                lua.print(&format!("{}", msg.config.to_string()));
            }
            WatchKind::Stop => {
                lua.print(&format!("Stopping watching service .."));
            }
        }

        Ok(())
    }
}

#[cfg(feature = "mlua")]
impl<'a> FromLua<'a> for WatchTarget {
    fn from_lua(lua_value: LuaValue<'a>, _lua: &'a Lua) -> LuaResult<Self> {
        if let LuaValue::Table(table) = lua_value {
            let ops = if table.get::<_, String>("ops")? == "Start" {
                WatchKind::Start
            } else {
                WatchKind::Stop
            };

            Ok(Self {
                client: table.get("client")?,
                config: table.get("config")?,
                ops,
            })
        } else {
            Err(LuaError::external("Fail to deserialize Watch"))
        }
    }
}

Move watch content to more specific scope.

/

/ Would make sesne if it's part of compile module, because I can't think of any other uses for

/ watching current directory other for recompiling purpose.

https://github.com/tami5/XcodeBase.nvim/blob/d5226d09d5f90de0186939ba058c1f236451bd43/src/util/watch.rs#L11

use tokio::sync::{mpsc, Mutex};
use wax::{Glob, Pattern};

/// TODO: Move watch content to more specific scope.
///
/// Would make sesne if it's part of compile module, because I can't think of any other uses for
/// watching current directory other for recompiling purpose.

/// Create new handler to watch workspace root.
#[cfg(feature = "daemon")]
pub fn handler(

[nvim] build log correct height

https://github.com/tami5/XcodeBase.nvim/blob/04ba82865312e6cea89dc19a030769b5f1f99a64/src/nvim/watchlogger.rs#L60

use super::{NvimClient, NvimConnection};
use crate::nvim::BufferDirection;
use crate::types::BuildConfiguration;
use anyhow::Result;
use nvim_rs::{Buffer, Window};
use tokio_stream::{Stream, StreamExt};

pub struct WatchLogger<'a> {
    pub nvim: &'a NvimClient,
    pub title: &'a str,
    pub request: &'a BuildConfiguration,
}

impl<'a> WatchLogger<'a> {
    pub fn new(nvim: &'a NvimClient, title: &'a str, request: &'a BuildConfiguration) -> Self {
        Self {
            nvim,
            title,
            request,
        }
    }

    pub async fn log_stream<S>(
        &self,
        mut stream: S,
        direction: Option<BufferDirection>,
        clear: bool,
        open: bool,
    ) -> Result<()>
    where
        S: Stream<Item = String> + Unpin,
    {
        let Self {
            nvim,
            title,
            request,
        } = self;

        let BuildConfiguration { .. } = request;

        nvim.exec("let g:xcodebase_watch_build_status='running'", false)
            .await?;

        let title = format!(
            "[{}] ------------------------------------------------------",
            title
        );

        let buf = Buffer::new(nvim.log_bufnr.into(), nvim.inner().clone());

        if clear {
            buf.set_lines(0, -1, false, vec![]).await?;
        }

        let mut c = match buf.line_count().await? {
            1 => 0,
            count => count,
        };

        // TODO(nvim): build log correct height
        // TODO(nvim): make auto clear configurable
        let command =
            match BufferDirection::get_window_direction(nvim, direction, nvim.log_bufnr).await {
                Ok(open_command) => open_command,
                Err(e) => {
                    tracing::error!("Unable to convert value to string {e}");
                    BufferDirection::Horizontal.to_nvim_command(nvim.log_bufnr)
                }
            };

        let mut win: Option<Window<NvimConnection>> = None;

        if open {
            nvim.exec(&command, false).await?;
            nvim.exec("setl nu nornu so=9", false).await?;
            win = Some(nvim.get_current_win().await?);
            nvim.exec("wincmd w", false).await?;
        }

        buf.set_lines(c, c + 1, false, vec![title]).await?;

        c += 1;

        let mut success = false;

        while let Some(line) = stream.next().await {
            if line.contains("Succeed") {
                success = true;
            }
            buf.set_lines(c, c + 1, false, vec![line]).await?;
            c += 1;
            if open {
                win.as_ref().unwrap().set_cursor((c, 0)).await?;
            }
        }

        if success {
            nvim.exec("let g:xcodebase_watch_build_status='success'", false)
                .await?;
        } else {
            nvim.exec("let g:xcodebase_watch_build_status='failure'", false)
                .await?;
            if !open {
                nvim.exec(&command, false).await?;
                nvim.get_current_win().await?.set_cursor((c, 0)).await?;
                nvim.exec("call feedkeys('zt')", false).await?;
            }
        }

        Ok(())
    }
}

[nvim] build log correct width

https://github.com/tami5/XcodeBase.nvim/blob/a9eede75989425943de53943e00fc1faa23f6d97/src/daemon/nvim.rs#L83

    pub async fn log_warn(&self, scope: &str, msg: impl ToString) -> Result<()> {
        self.log("warn", scope, msg).await
    }

    pub async fn log_to_buffer(
        &self,
        title: &str,
        direction: WindowType,
        mut stream: impl tokio_stream::Stream<Item = String> + Unpin,
        clear: bool,
    ) -> Result<()> {
        let title = format!("[ {title} ]: ----> ");
        let buffer = Buffer::new(self.log_bufnr.into(), self.nvim.clone());

        if clear {
            buffer.set_lines(0, -1, false, vec![]).await?;
        }

        let mut c = match buffer.line_count().await? {
            1 => 0,
            count => count,
        };

        // TODO(nvim): build log control what direction to open buffer
        // TODO(nvim): build log correct width
        // TODO(nvim): build log auto scroll
        let command = match direction {
            // TOOD: build log float
            WindowType::Float => format!("sbuffer {}", self.log_bufnr),
            WindowType::Vertical => format!("vert sbuffer {}", self.log_bufnr),
            WindowType::Horizontal => format!("sbuffer {}", self.log_bufnr),
        };

        self.exec(&command, false).await?;

        buffer.set_lines(c, c + 1, false, vec![title]).await?;
        c += 1;

        while let Some(line) = stream.next().await {
            buffer.set_lines(c, c + 1, false, vec![line]).await?;
            c += 1
        }

        Ok(())
    }
}

impl std::fmt::Debug for Nvim {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_tuple("Nvim").finish()
    }
}

impl ops::Deref for Nvim {
    type Target = Neovim<Compat<WriteHalf<Connection>>>;
    fn deref(&self) -> &Self::Target {
        &self.nvim
    }
}

Find away to get commands ran without doing xcodebuild clean

Right now, in order to produce compiled commands and for xcodebuild build to spit out ran

commands, we need to first run xcodebuild clean.

NOTE: This far from possilbe after some research

https://github.com/tami5/XcodeBase.nvim/blob/a87a4ff04529327cc5d8f8093e91118478b6c44b/shared/src/project.rs#L65

    pub fn targets(&self) -> &TargetMap {
        &self.targets
    }

    /// Build project with clean and return build log
    pub async fn fresh_build(&self) -> Result<Vec<String>> {
        /*
           TODO: Find away to get commands ran without doing xcodebuild clean

           Right now, in order to produce compiled commands and for `xcodebuild build` to spit out ran
           commands, we need to first run xcodebuild clean.

           NOTE: This far from possilbe after some research
        */
        xcode::clean(&self.root, &["-verbose"]).await?;

        /*
           TODO: Support overriding xcodebuild arguments

           Not sure how important is this, but ideally I'd like to be able to add extra arguments for
           when generating compiled commands, as well as doing actual builds and runs.

           ```yaml
           XcodeBase:
           buildArguments: [];
           compileArguments: [];
           runArguments: [];
           ```
        */
        xcode::build(&self.root, &["-verbose"]).await
    }
}

impl Project {}

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.