diff --git a/Cargo.lock b/Cargo.lock index 737c315..a265f22 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,12 +8,12 @@ dependencies = [ [[package]] name = "atk-sys" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "bitflags 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", - "glib-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", - "gobject-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", + "glib-sys 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "gobject-sys 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)", "pkg-config 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -50,25 +50,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "cairo-rs" -version = "0.3.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "c_vec 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", - "cairo-sys-rs 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", - "glib 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", - "glib-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", + "cairo-sys-rs 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "glib 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", + "glib-sys 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "cairo-sys-rs" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "libc 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)", "pkg-config 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -118,103 +117,106 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "gdk" -version = "0.7.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "bitflags 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", - "cairo-rs 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", - "cairo-sys-rs 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", - "gdk-pixbuf 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", - "gdk-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", - "gio 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", - "glib 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", - "glib-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", - "gobject-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", + "cairo-rs 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", + "cairo-sys-rs 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "gdk-pixbuf 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "gdk-sys 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "gio 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "glib 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", + "glib-sys 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "gobject-sys 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)", - "pango 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "pango 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "gdk-pixbuf" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "gdk-pixbuf-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", - "glib 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", - "glib-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", - "gobject-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", + "gdk-pixbuf-sys 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "gio 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "gio-sys 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "glib 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", + "glib-sys 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "gobject-sys 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "gdk-pixbuf-sys" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "bitflags 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", - "gio-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", - "glib-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", - "gobject-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", + "gio-sys 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "glib-sys 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "gobject-sys 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)", "pkg-config 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "gdk-sys" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "bitflags 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", - "cairo-sys-rs 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", - "gdk-pixbuf-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", - "gio-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", - "glib-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", - "gobject-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", + "cairo-sys-rs 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "gdk-pixbuf-sys 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "gio-sys 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "glib-sys 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "gobject-sys 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)", - "pango-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", + "pango-sys 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", "pkg-config 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "gio" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "bitflags 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", - "gio-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", - "glib 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", - "glib-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", - "gobject-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", + "gio-sys 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "glib 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", + "glib-sys 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "gobject-sys 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "gio-sys" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "bitflags 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", - "glib-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", - "gobject-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", + "glib-sys 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "gobject-sys 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)", "pkg-config 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "glib" -version = "0.4.1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "bitflags 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", - "glib-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", - "gobject-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", + "glib-sys 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "gobject-sys 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", "lazy_static 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "glib-sys" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "bitflags 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", @@ -224,52 +226,53 @@ dependencies = [ [[package]] name = "gobject-sys" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "bitflags 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", - "glib-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", + "glib-sys 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)", "pkg-config 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "gtk" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "bitflags 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", - "cairo-rs 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", - "cairo-sys-rs 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", - "gdk 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", - "gdk-pixbuf 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", - "gdk-pixbuf-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", - "gdk-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", - "gio 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", - "gio-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", - "glib 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", - "glib-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", - "gobject-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", - "gtk-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", + "cairo-rs 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", + "cairo-sys-rs 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "gdk 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", + "gdk-pixbuf 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "gdk-pixbuf-sys 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "gdk-sys 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "gio 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "gio-sys 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "glib 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", + "glib-sys 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "gobject-sys 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "gtk-sys 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)", - "pango 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "pango 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "gtk-sys" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "atk-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", + "atk-sys 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", "bitflags 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", - "cairo-sys-rs 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", - "gdk-pixbuf-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", - "gdk-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", - "gio-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", - "glib-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", - "gobject-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", + "cairo-sys-rs 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "gdk-pixbuf-sys 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "gdk-sys 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "gio-sys 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "glib-sys 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "gobject-sys 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)", - "pango-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", + "pango-sys 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", "pkg-config 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -364,86 +367,96 @@ source = "registry+https://github.com/rust-lang/crates.io-index" name = "nvim-gtk" version = "0.2.0" dependencies = [ - "cairo-rs 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "cairo-rs 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", "env_logger 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)", - "gdk 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", - "gdk-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", - "gio 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", - "glib 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", - "glib-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", - "gobject-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", - "gtk 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", - "gtk-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", + "gdk 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", + "gdk-sys 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "gio 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "glib 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", + "glib-sys 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "gobject-sys 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "gtk 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "gtk-sys 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", "htmlescape 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", "neovim-lib 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", - "pango 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", - "pango-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", - "pangocairo 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", - "pangocairo-sys 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "pango 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "pango-sys 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "pangocairo 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", + "pangocairo-sys 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", + "percent-encoding 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", "phf 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)", "phf_codegen 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", + "rmpv 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.27 (registry+https://github.com/rust-lang/crates.io-index)", "serde_derive 1.0.27 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)", "toml 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-width 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "pango" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "bitflags 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", - "glib 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", - "glib-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", - "gobject-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", + "glib 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", + "glib-sys 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "gobject-sys 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)", - "pango-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", + "pango-sys 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "pango-sys" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "bitflags 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", - "glib-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", - "gobject-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", + "glib-sys 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "gobject-sys 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)", "pkg-config 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "pangocairo" -version = "0.4.1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "bitflags 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", - "cairo-rs 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", - "cairo-sys-rs 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", - "glib 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", - "glib-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", - "gobject-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", + "cairo-rs 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", + "cairo-sys-rs 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "glib 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", + "glib-sys 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "gobject-sys 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)", - "pango 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", - "pango-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", - "pangocairo-sys 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "pango 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "pango-sys 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "pangocairo-sys 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "pangocairo-sys" -version = "0.6.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "bitflags 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", - "cairo-sys-rs 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", - "glib-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", + "cairo-sys-rs 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "glib-sys 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)", - "pango-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", + "pango-sys 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", "pkg-config 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "percent-encoding" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "phf" version = "0.7.21" @@ -669,6 +682,11 @@ dependencies = [ "serde 1.0.27 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "unicode-width" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "unicode-xid" version = "0.0.4" @@ -701,11 +719,6 @@ name = "void" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -[[package]] -name = "winapi" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" - [[package]] name = "winapi" version = "0.3.4" @@ -735,31 +748,31 @@ dependencies = [ [metadata] "checksum aho-corasick 0.6.4 (registry+https://github.com/rust-lang/crates.io-index)" = "d6531d44de723825aa81398a6415283229725a00fa30713812ab9323faa82fc4" -"checksum atk-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "33a67fd81e1922dddc335887516f2f5254534e89c9d39fa89bca5d79bd150d34" +"checksum atk-sys 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f8dc233521f7bffd3042c31082ea71bd08820abf44bac938fb36591e20f76f39" "checksum atty 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "8352656fd42c30a0c3c89d26dea01e3b77c0ab2af18230835c15e2e13cd51859" "checksum bitflags 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)" = "4efd02e230a02e18f92fc2735f44597385ed02ad8f831e7c1c1156ee5e1ab3a5" "checksum bitflags 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b3c30d3802dfb7281680d6285f2ccdaa8c2d8fee41f93805dba5c4cf50dc23cf" "checksum byteorder 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "652805b7e73fada9d85e9a6682a4abd490cb52d96aeecc12e33a0de34dfd0d23" "checksum c_vec 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "6237ac5a4b1e81c213c24c6437964c61e646df910a914b4ab1487b46df20bd13" -"checksum cairo-rs 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b6b5695f59fd036fe5741bc5a4eb20c78fbe42256e3b08a2af26bbcbe8070bf3" -"checksum cairo-sys-rs 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7c6e18fecaeac51809db57f45f4553cc0975225a7eb435a7a7e91e5e8113a84d" +"checksum cairo-rs 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a110f269c2fd382df5fe8bd46dfa5f1b83608aa717fecb6e7a28c08c202f0e13" +"checksum cairo-sys-rs 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "0395175ecba60accac076a02c31d143b9dcd9d5eb5316d7163a3273803b765c7" "checksum cfg-if 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "d4c819a1287eb618df47cc647173c5c4c66ba19d888a6e50d605672aed3140de" "checksum chrono 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7c20ebe0b2b08b0aeddba49c609fe7957ba2e33449882cb186a180bc60682fa9" "checksum dtoa 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "09c3753c3db574d215cba4ea76018483895d7bff25a31b49ba45db21c48e50ab" "checksum env_logger 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "f15f0b172cb4f52ed5dbf47f774a387cd2315d1bf7894ab5af9b083ae27efa5a" "checksum fuchsia-zircon 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82" "checksum fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" -"checksum gdk 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a1e51db95be6565011bcd5cd99f9b17fdd585001057a999b21e09f1e8c28deb9" -"checksum gdk-pixbuf 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "16160d212ae91abe9f3324c3fb233929ba322dde63585d15cda3336f8c529ed1" -"checksum gdk-pixbuf-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "798f97101eea8180da363d0e80e07ec7ec6d1809306601c0100c1de5bc8b4f52" -"checksum gdk-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d4ee916f5f25c5f4b21bd9dcb12a216ae697406940ff9476358c308a8ececada" -"checksum gio 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "84ba5a2beb559059a0c9c2bd3681743cdede8d9a36c775840bca800333b22867" -"checksum gio-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a303bbf7a5e75ab3b627117ff10e495d1b9e97e1d68966285ac2b1f6270091bc" -"checksum glib 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b9b0452824cc63066940f01adc721804919f0b76cdba3cfab977b00b87f16d4a" -"checksum glib-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d9693049613ff52b93013cc3d2590366d8e530366d288438724b73f6c7dc4be8" -"checksum gobject-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "60d507c87a71b1143c66ed21a969be9b99a76df234b342d733e787e6c9c7d7c2" -"checksum gtk 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "0847c507e52c1feaede13ef56fb4847742438602655449d5f1f782e8633f146f" -"checksum gtk-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "905fcfbaaad1b44ec0b4bba9e4d527d728284c62bc2ba41fccedace2b096766f" +"checksum gdk 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "dd30051ff3d908ff2fc7e5776ffe1c699821e043809f294c3a61004f11d6c3a9" +"checksum gdk-pixbuf 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c2d2199eba47ebcb9977ce28179649bdd59305ef465c4e6f9b65aaa41c24e6b5" +"checksum gdk-pixbuf-sys 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "df6a3b73e04fafc07f5ebc083f1096a773412e627828e1103a55e921f81187d8" +"checksum gdk-sys 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3162ff940526ddff71bf1f630facee6b5e05d282d125ba0c4c803842819b80c3" +"checksum gio 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "071fd4e5592b39fdc31318e32d2497842501f539bcedb60c75cd365946642adc" +"checksum gio-sys 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2a57872499171d279f8577ce83837da4cae62b08dd32892236ed67ab7ea61030" +"checksum glib 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5e0be1b1432e227bcd1a9b28db9dc1474a7e7fd4227e08e16f35304f32d09b61" +"checksum glib-sys 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "615bef979b5838526aee99241afc80cfb2e34a8735d4bcb8ec6072598c18a408" +"checksum gobject-sys 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "70409d6405db8b1591602fcd0cbe8af52cd9976dd39194442b4c149ba343f86d" +"checksum gtk 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "414f3522f550a0b4f65e089f00ffcd3987dab8b0be284cb979aa7f6a03d60516" +"checksum gtk-sys 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3d9554cf5b3a85a13fb39258c65b04b262989c1d7a758f8f555b77a478621a91" "checksum htmlescape 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "e9025058dae765dee5070ec375f591e2ba14638c63feff74f13805a72e523163" "checksum itoa 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "8324a32baf01e2ae060e9de58ed0bc2320c9a2833491ee36cd3b4c414de4db8c" "checksum lazy_static 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c8f31047daa365f19be14b47c29df4f7c3b581832407daabe6ae77397619237d" @@ -772,10 +785,11 @@ dependencies = [ "checksum num-iter 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)" = "4b226df12c5a59b63569dd57fafb926d91b385dfce33d8074a412411b689d593" "checksum num-traits 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)" = "92e5113e9fd4cc14ded8e499429f396a20f98c772a47cc8622a736e1ec843c31" "checksum num-traits 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e7de20f146db9d920c45ee8ed8f71681fd9ade71909b48c3acbd766aa504cf10" -"checksum pango 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3e81c404ab81ea7ea2fc2431a0a7672507b80e4b8bf4b41eac3fc83cc665104e" -"checksum pango-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "34f34a1be107fe16abb2744e0e206bee4b3b07460b5fddd3009a6aaf60bd69ab" -"checksum pangocairo 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "41a8620ece55098d741bacf4d3aa52398f85ce83cfe0d8f670fa11de88f52c40" -"checksum pangocairo-sys 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e34ec010b38a2a6dafdf3d86ffe1251a0ae759208ec85f78c66c0445481bd5a8" +"checksum pango 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "45374801e224373c3c0393cd48073c81093494c8735721e81d1dbaa4096b2767" +"checksum pango-sys 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "94039b3921a4af4058a3e4335e5d15099101f298a92f5afc40bab3a3027594a1" +"checksum pangocairo 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "db4130d0e0567b177178c9568466277ff167cb43a245b2881e203509ea5fbd84" +"checksum pangocairo-sys 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "decc9523cd5bbba49a67ed8d2ef7dfca957f4760bf420a1ea4a82634da26381c" +"checksum percent-encoding 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "31010dd2e1ac33d5b46a5b413495239882813e0369f8ed8a5e266f173602f831" "checksum phf 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)" = "cb325642290f28ee14d8c6201159949a872f220c62af6e110a56ea914fbe42fc" "checksum phf_codegen 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)" = "d62594c0bb54c464f633175d502038177e90309daf2e0158be42ed5f023ce88f" "checksum phf_generator 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)" = "6b07ffcc532ccc85e3afc45865469bf5d9e4ef5bfcf9622e3cfe80c2d275ec03" @@ -803,12 +817,12 @@ dependencies = [ "checksum thread_local 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "279ef31c19ededf577bfd12dfae728040a21f635b06a24cd670ff510edd38963" "checksum time 0.1.39 (registry+https://github.com/rust-lang/crates.io-index)" = "a15375f1df02096fb3317256ce2cee6a1f42fc84ea5ad5fc8c421cfe40c73098" "checksum toml 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)" = "a7540f4ffc193e0d3c94121edb19b055670d369f77d5804db11ae053a45b6e7e" +"checksum unicode-width 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "bf3a113775714a22dcb774d8ea3655c53a32debae63a063acc00a91cc586245f" "checksum unicode-xid 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "8c1f860d7d29cf02cb2f3f359fd35991af3d30bac52c57d265a3c461074cb4dc" "checksum unix_socket 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "6aa2700417c405c38f5e6902d699345241c28c0b7ade4abaad71e35a87eb1564" "checksum unreachable 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "382810877fe448991dfc7f0dd6e3ae5d58088fd0ea5e35189655f84e6814fa56" "checksum utf8-ranges 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "662fab6525a98beff2921d7f61a39e7d59e0b425ebc7d0d9e66d316e55124122" "checksum void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" -"checksum winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" "checksum winapi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "04e3bd221fcbe8a271359c04f21a76db7d0c6028862d1bb5512d85e1e2eb5bb3" "checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" "checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/Cargo.toml b/Cargo.toml index fe5f19e..32a7b5c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,17 +5,17 @@ authors = ["daa84 "] build = "build.rs" [dependencies] -cairo-rs = "0.3" -pango = "0.3" -pango-sys = "0.5" -pangocairo = "0.4" -pangocairo-sys = "0.6" -glib = "0.4" -glib-sys = "0.5" -gdk = "0.7" -gdk-sys = "0.5" -gio = "0.3" -gobject-sys = "0.5" +cairo-rs = "0.4" +pango = "0.4" +pango-sys = "0.6" +pangocairo = "0.5" +pangocairo-sys = "0.7" +glib = "0.5" +glib-sys = "0.6" +gdk = "0.8" +gdk-sys = "0.6" +gio = "0.4" +gobject-sys = "0.6" #gdk = { git = 'https://github.com/gtk-rs/gdk' } #gdk-sys = { git = 'https://github.com/gtk-rs/sys' } #glib = { git = 'https://github.com/gtk-rs/glib' } @@ -31,6 +31,11 @@ phf = "0.7" log = "0.4" env_logger = "0.5" htmlescape = "0.3" +rmpv = "0.4" +percent-encoding = "1.0" +regex = "0.2" +lazy_static = "1.0" +unicode-width = "0.1.4" serde = "1.0" serde_derive = "1.0" @@ -44,12 +49,12 @@ serde_json = "1.0" phf_codegen = "0.7" [dependencies.gtk] -version = "0.3" +version = "0.4" features = ["v3_22"] #git = "https://github.com/gtk-rs/gtk" [dependencies.gtk-sys] -version = "0.5" +version = "0.6" features = ["v3_22"] #git = 'https://github.com/gtk-rs/sys' diff --git a/appveyor.yml b/appveyor.yml index 41caa0b..262de55 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -3,7 +3,7 @@ environment: PROJECT_NAME: neovim-gtk-win64 matrix: - TARGET: x86_64-pc-windows-gnu - RUST_VERSION: 1.22.1 + RUST_VERSION: 1.24.0 install: # - ps: Start-FileDownload "https://static.rust-lang.org/dist/channel-rust-stable" diff --git a/resources/side-panel.ui b/resources/side-panel.ui new file mode 100644 index 0000000..2e595cb --- /dev/null +++ b/resources/side-panel.ui @@ -0,0 +1,141 @@ + + + + + + + + + + + + + + + + True + False + + + True + False + filebrowser.cd + Go To Directory + + + + + True + False + + + + + True + False + filebrowser.reload + Reload + + + + + True + False + Show Hidden Files + + + + + + + + + + + + + + + + + 150 + False + vertical + + + False + False + 6 + dir_list_model + 1 + + + 6 + + + 1 + + + + + 6 + end + + + 0 + + + + + False + True + 0 + + + + + False + + + False + file_browser_tree_store + False + False + 20 + True + + + + + + autosize + + + 6 + + + 3 + + + + + + 0 + + + + + + + + + True + True + 1 + + + + + diff --git a/runtime/plugin/nvim_gui_shim.vim b/runtime/plugin/nvim_gui_shim.vim index 565b2ff..2815e55 100644 --- a/runtime/plugin/nvim_gui_shim.vim +++ b/runtime/plugin/nvim_gui_shim.vim @@ -54,3 +54,6 @@ function s:GuiFontCommand(fname, bang) abort endfunction command! -nargs=? -bang Guifont call s:GuiFontCommand("", "") command! -nargs=? -bang GuiFont call s:GuiFontCommand("", "") + +command! NGToggleSidebar call rpcnotify(1, 'Gui', 'Command', 'ToggleSidebar') + diff --git a/screenshots/neovimgtk-screen.png b/screenshots/neovimgtk-screen.png index 05f745c..cc21790 100644 Binary files a/screenshots/neovimgtk-screen.png and b/screenshots/neovimgtk-screen.png differ diff --git a/src/cmd_line.rs b/src/cmd_line.rs new file mode 100644 index 0000000..6ab4bc2 --- /dev/null +++ b/src/cmd_line.rs @@ -0,0 +1,481 @@ +use std::collections::HashMap; +use std::rc::Rc; +use std::sync::Arc; +use std::cell::RefCell; +use std::cmp::max; + +use gtk; +use gtk::prelude::*; +use cairo; + +use neovim_lib::Value; + +use ui_model::{Attrs, ModelLayout}; +use ui::UiMutex; +use render::{self, CellMetrics}; +use shell; +use cursor; + +pub struct Level { + model_layout: ModelLayout, + prompt_offset: usize, + preferred_width: i32, + preferred_height: i32, +} + +impl Level { + pub fn insert(&mut self, c: &str, shift: bool, render_state: &shell::RenderState) { + self.model_layout.insert_char(c, shift); + self.update_preferred_size(render_state); + } + + pub fn replace_from_ctx(&mut self, ctx: &CmdLineContext, render_state: &shell::RenderState) { + let content = ctx.get_lines(); + self.replace_line(content.lines, false); + self.prompt_offset = content.prompt_offset; + self.model_layout + .set_cursor(self.prompt_offset + ctx.pos as usize); + self.update_preferred_size(render_state); + } + + pub fn from_ctx(ctx: &CmdLineContext, render_state: &shell::RenderState) -> Self { + let content = ctx.get_lines(); + let mut level = Level::from_lines(content.lines, ctx.max_width, render_state); + + level.prompt_offset = content.prompt_offset; + level + .model_layout + .set_cursor(level.prompt_offset + ctx.pos as usize); + level.update_preferred_size(render_state); + + level + } + + fn replace_line(&mut self, lines: Vec, Vec)>>, append: bool) { + if append { + self.model_layout.layout_append(lines); + } else { + self.model_layout.layout(lines); + } + } + + fn update_preferred_size(&mut self, render_state: &shell::RenderState) { + let &CellMetrics { + line_height, + char_width, + .. + } = render_state.font_ctx.cell_metrics(); + + let (columns, rows) = self.model_layout.size(); + let columns = max(columns, 5); + + self.preferred_width = (char_width * columns as f64) as i32; + self.preferred_height = (line_height * rows as f64) as i32; + } + + fn to_attributed_content( + content: &Vec, String)>>, + ) -> Vec, Vec)>> { + content + .iter() + .map(|line_chars| { + line_chars + .iter() + .map(|c| (Some(Attrs::from_value_map(&c.0)), c.1.chars().collect())) + .collect() + }) + .collect() + } + + pub fn from_multiline_content( + content: &Vec, String)>>, + max_width: i32, + render_state: &shell::RenderState, + ) -> Self { + Level::from_lines( + Level::to_attributed_content(content), + max_width, + render_state, + ) + } + + pub fn from_lines( + lines: Vec, Vec)>>, + max_width: i32, + render_state: &shell::RenderState, + ) -> Self { + let &CellMetrics { char_width, .. } = render_state.font_ctx.cell_metrics(); + + let max_width_chars = (max_width as f64 / char_width) as u64; + + let mut model_layout = ModelLayout::new(max_width_chars); + model_layout.layout(lines); + + let mut level = Level { + model_layout, + preferred_width: -1, + preferred_height: -1, + prompt_offset: 0, + }; + + level.update_preferred_size(render_state); + level + } + + fn update_cache(&mut self, render_state: &shell::RenderState) { + render::shape_dirty( + &render_state.font_ctx, + &mut self.model_layout.model, + &render_state.color_model, + ); + } + + fn set_cursor(&mut self, render_state: &shell::RenderState, pos: usize) { + self.model_layout.set_cursor(self.prompt_offset + pos); + self.update_preferred_size(render_state); + } +} + +fn prompt_lines( + firstc: &str, + prompt: &str, + indent: u64, +) -> (usize, Vec<(Option, Vec)>) { + let prompt: Vec<(Option, Vec)> = if !firstc.is_empty() { + if firstc.len() >= indent as usize { + vec![(None, firstc.chars().collect())] + } else { + vec![ + ( + None, + firstc + .chars() + .chain((firstc.len()..indent as usize).map(|_| ' ')) + .collect(), + ), + ] + } + } else if !prompt.is_empty() { + prompt + .lines() + .map(|l| (None, l.chars().collect())) + .collect() + } else { + vec![] + }; + + let prompt_offset = prompt.last().map(|l| l.1.len()).unwrap_or(0); + + (prompt_offset, prompt) +} + +struct State { + levels: Vec, + block: Option, + render_state: Rc>, + drawing_area: gtk::DrawingArea, + cursor: Option>, +} + +impl State { + fn new(drawing_area: gtk::DrawingArea, render_state: Rc>) -> Self { + State { + levels: Vec::new(), + block: None, + render_state, + drawing_area, + cursor: None, + } + } + + fn request_area_size(&self) { + let drawing_area = self.drawing_area.clone(); + let block = self.block.as_ref(); + let level = self.levels.last(); + + let (block_width, block_height) = block + .map(|b| (b.preferred_width, b.preferred_height)) + .unwrap_or((0, 0)); + let (level_width, level_height) = level + .map(|l| (l.preferred_width, l.preferred_height)) + .unwrap_or((0, 0)); + + drawing_area.set_size_request( + max(level_width, block_width), + max(block_height + level_height, 40), + ); + } + + fn preferred_height(&self) -> i32 { + let level = self.levels.last(); + level.map(|l| l.preferred_height).unwrap_or(0) + + self.block.as_ref().map(|b| b.preferred_height).unwrap_or(0) + } + + fn set_cursor(&mut self, render_state: &shell::RenderState, pos: usize, level: usize) { + debug_assert!(level > 0); + + // queue old cursor position + self.queue_redraw_cursor(); + + self.levels + .get_mut(level - 1) + .map(|l| l.set_cursor(render_state, pos)); + } + + fn queue_redraw_cursor(&mut self) { + if let Some(ref level) = self.levels.last() { + let level_preferred_height = level.preferred_height; + let block_preferred_height = + self.block.as_ref().map(|b| b.preferred_height).unwrap_or(0); + + let gap = self.drawing_area.get_allocated_height() - level_preferred_height + - block_preferred_height; + + let model = &level.model_layout.model; + + let mut cur_point = model.cur_point(); + cur_point.extend_by_items(model); + + let render_state = self.render_state.borrow(); + let cell_metrics = render_state.font_ctx.cell_metrics(); + + let (x, y, width, height) = cur_point.to_area_extend_ink(model, cell_metrics); + + if gap > 0 { + self.drawing_area + .queue_draw_area(x, y + gap / 2, width, height); + } else { + self.drawing_area + .queue_draw_area(x, y + block_preferred_height, width, height); + } + } + } +} + +impl cursor::CursorRedrawCb for State { + fn queue_redraw_cursor(&mut self) { + self.queue_redraw_cursor(); + } +} + +pub struct CmdLine { + popover: gtk::Popover, + displyed: bool, + state: Arc>, +} + +impl CmdLine { + pub fn new(drawing: >k::DrawingArea, render_state: Rc>) -> Self { + let popover = gtk::Popover::new(Some(drawing)); + popover.set_modal(false); + popover.set_position(gtk::PositionType::Right); + + let drawing_area = gtk::DrawingArea::new(); + drawing_area.show_all(); + popover.add(&drawing_area); + + let state = Arc::new(UiMutex::new(State::new(drawing_area.clone(), render_state))); + let weak_cb = Arc::downgrade(&state); + let cursor = cursor::BlinkCursor::new(weak_cb); + state.borrow_mut().cursor = Some(cursor); + + drawing_area.connect_draw(clone!(state => move |_, ctx| gtk_draw(ctx, &state))); + + CmdLine { + popover, + state, + displyed: false, + } + } + + pub fn show_level(&mut self, ctx: &CmdLineContext) { + let mut state = self.state.borrow_mut(); + let render_state = state.render_state.clone(); + let render_state = render_state.borrow(); + + if ctx.level_idx as usize == state.levels.len() { + let level = state.levels.last_mut().unwrap(); + level.replace_from_ctx(ctx, &*render_state); + level.update_cache(&*render_state); + } else { + let mut level = Level::from_ctx(ctx, &*render_state); + level.update_cache(&*render_state); + state.levels.push(level); + } + + state.request_area_size(); + + if !self.displyed { + self.displyed = true; + self.popover.set_pointing_to(>k::Rectangle { + x: ctx.x, + y: ctx.y, + width: ctx.width, + height: ctx.height, + }); + + self.popover.popup(); + state.cursor.as_mut().unwrap().start(); + } else { + state.drawing_area.queue_draw() + } + } + + pub fn special_char( + &self, + render_state: &shell::RenderState, + c: String, + shift: bool, + level: u64, + ) { + let mut state = self.state.borrow_mut(); + + if let Some(level) = state.levels.get_mut((level - 1) as usize) { + level.insert(&c, shift, render_state); + level.update_cache(&*render_state); + } else { + error!("Level {} does not exists", level); + } + + state.request_area_size(); + state.drawing_area.queue_draw() + } + + pub fn hide_level(&mut self, level_idx: u64) { + let mut state = self.state.borrow_mut(); + + if level_idx as usize == state.levels.len() { + state.levels.pop(); + } + + if state.levels.is_empty() { + self.popover.hide(); + self.displyed = false; + state.cursor.as_mut().unwrap().leave_focus(); + } + } + + pub fn show_block( + &mut self, + content: &Vec, String)>>, + max_width: i32, + ) { + let mut state = self.state.borrow_mut(); + let mut block = + Level::from_multiline_content(content, max_width, &*state.render_state.borrow()); + block.update_cache(&*state.render_state.borrow()); + state.block = Some(block); + state.request_area_size(); + } + + pub fn block_append(&mut self, content: &Vec<(HashMap, String)>) { + let mut state = self.state.borrow_mut(); + let render_state = state.render_state.clone(); + { + let attr_content = content + .iter() + .map(|c| (Some(Attrs::from_value_map(&c.0)), c.1.chars().collect())) + .collect(); + + let block = state.block.as_mut().unwrap(); + block.replace_line(vec![attr_content], true); + block.update_preferred_size(&*render_state.borrow()); + block.update_cache(&*render_state.borrow()); + } + state.request_area_size(); + } + + pub fn block_hide(&mut self) { + self.state.borrow_mut().block = None; + } + + pub fn pos(&mut self, render_state: &shell::RenderState, pos: u64, level: u64) { + self.state + .borrow_mut() + .set_cursor(render_state, pos as usize, level as usize); + } +} + +fn gtk_draw(ctx: &cairo::Context, state: &Arc>) -> Inhibit { + let state = state.borrow(); + let preferred_height = state.preferred_height(); + let level = state.levels.last(); + let block = state.block.as_ref(); + + let render_state = state.render_state.borrow(); + + let gap = state.drawing_area.get_allocated_height() - preferred_height; + if gap > 0 { + ctx.translate(0.0, (gap / 2) as f64); + } + + render::clear(ctx, &render_state.color_model); + + if let Some(block) = block { + render::render( + ctx, + &cursor::EmptyCursor::new(), + &render_state.font_ctx, + &block.model_layout.model, + &render_state.color_model, + &render_state.mode, + ); + + ctx.translate(0.0, block.preferred_height as f64); + } + + if let Some(level) = level { + render::render( + ctx, + state.cursor.as_ref().unwrap(), + &render_state.font_ctx, + &level.model_layout.model, + &render_state.color_model, + &render_state.mode, + ); + } + Inhibit(false) +} + +pub struct CmdLineContext { + pub content: Vec<(HashMap, String)>, + pub pos: u64, + pub firstc: String, + pub prompt: String, + pub indent: u64, + pub level_idx: u64, + pub x: i32, + pub y: i32, + pub width: i32, + pub height: i32, + pub max_width: i32, +} + +impl CmdLineContext { + fn get_lines(&self) -> LineContent { + let content_line: Vec<(Option, Vec)> = self.content + .iter() + .map(|c| (Some(Attrs::from_value_map(&c.0)), c.1.chars().collect())) + .collect(); + let (prompt_offset, prompt_lines) = prompt_lines(&self.firstc, &self.prompt, self.indent); + + let mut content: Vec<_> = prompt_lines.into_iter().map(|line| vec![line]).collect(); + + if content.is_empty() { + content.push(content_line); + } else { + content.last_mut().map(|line| line.extend(content_line)); + } + + LineContent { + lines: content, + prompt_offset, + } + } +} + +struct LineContent { + lines: Vec, Vec)>>, + prompt_offset: usize, +} diff --git a/src/cursor.rs b/src/cursor.rs index b478c97..8c5068b 100644 --- a/src/cursor.rs +++ b/src/cursor.rs @@ -1,10 +1,8 @@ use cairo; use color::Color; use ui::UiMutex; -use shell; use mode; use nvim; -use nvim::{RepaintMode, RedrawEvents}; use std::sync::{Arc, Weak}; use render; use render::CellMetrics; @@ -44,20 +42,20 @@ enum AnimPhase { Busy, } -struct State { +struct State { alpha: Alpha, anim_phase: AnimPhase, - shell: Weak>, + redraw_cb: Weak>, timer: Option, } -impl State { - fn new(shell: Weak>) -> State { +impl State { + fn new(redraw_cb: Weak>) -> Self { State { alpha: Alpha(1.0), anim_phase: AnimPhase::Shown, - shell: shell, + redraw_cb, timer: None, } } @@ -71,13 +69,48 @@ impl State { } } -pub struct Cursor { - state: Arc>, +pub trait Cursor { + fn draw( + &self, + ctx: &cairo::Context, + font_ctx: &render::Context, + mode: &mode::Mode, + line_y: f64, + double_width: bool, + bg: &Color, + ); } -impl Cursor { - pub fn new(shell: Weak>) -> Cursor { - Cursor { state: Arc::new(UiMutex::new(State::new(shell))) } +pub struct EmptyCursor; + +impl EmptyCursor { + pub fn new() -> Self { + EmptyCursor { } + } +} + +impl Cursor for EmptyCursor { + fn draw( + &self, + _ctx: &cairo::Context, + _font_ctx: &render::Context, + _mode: &mode::Mode, + _line_y: f64, + _double_width: bool, + _bg: &Color, + ) { + } +} + +pub struct BlinkCursor { + state: Arc>>, +} + +impl BlinkCursor { + pub fn new(redraw_cb: Weak>) -> Self { + BlinkCursor { + state: Arc::new(UiMutex::new(State::new(redraw_cb))), + } } pub fn start(&mut self) { @@ -112,8 +145,10 @@ impl Cursor { pub fn busy_off(&mut self) { self.start(); } +} - pub fn draw( +impl Cursor for BlinkCursor { + fn draw( &self, ctx: &cairo::Context, font_ctx: &render::Context, @@ -122,7 +157,6 @@ impl Cursor { double_width: bool, bg: &Color, ) { - let state = self.state.borrow(); if state.anim_phase == AnimPhase::Busy { @@ -157,9 +191,7 @@ fn cursor_rect( if let Some(mode_info) = mode.mode_info() { match mode_info.cursor_shape() { - None | - Some(&nvim::CursorShape::Unknown) | - Some(&nvim::CursorShape::Block) => { + None | Some(&nvim::CursorShape::Unknown) | Some(&nvim::CursorShape::Block) => { let cursor_width = if double_width { char_width * 2.0 } else { @@ -204,7 +236,8 @@ fn cursor_rect( (line_y, cursor_width, line_height) } } -fn anim_step(state: &Arc>) -> glib::Continue { + +fn anim_step(state: &Arc>>) -> glib::Continue { let mut mut_state = state.borrow_mut(); let next_event = match mut_state.anim_phase { @@ -212,37 +245,32 @@ fn anim_step(state: &Arc>) -> glib::Continue { mut_state.anim_phase = AnimPhase::Hide; Some(60) } - AnimPhase::Hide => { - if !mut_state.alpha.hide(0.3) { - mut_state.anim_phase = AnimPhase::Hidden; + AnimPhase::Hide => if !mut_state.alpha.hide(0.3) { + mut_state.anim_phase = AnimPhase::Hidden; - Some(300) - } else { - None - } - } + Some(300) + } else { + None + }, AnimPhase::Hidden => { mut_state.anim_phase = AnimPhase::Show; Some(60) } - AnimPhase::Show => { - if !mut_state.alpha.show(0.3) { - mut_state.anim_phase = AnimPhase::Shown; + AnimPhase::Show => if !mut_state.alpha.show(0.3) { + mut_state.anim_phase = AnimPhase::Shown; - Some(500) - } else { - None - } - } - AnimPhase::NoFocus => None, + Some(500) + } else { + None + }, + AnimPhase::NoFocus => None, AnimPhase::Busy => None, }; - let shell = mut_state.shell.upgrade().unwrap(); - let mut shell = shell.borrow_mut(); - let point = shell.model.cur_point(); - shell.on_redraw(&RepaintMode::Area(point)); + let redraw_cb = mut_state.redraw_cb.upgrade().unwrap(); + let mut redraw_cb = redraw_cb.borrow_mut(); + redraw_cb.queue_redraw_cursor(); if let Some(timeout) = next_event { @@ -253,10 +281,9 @@ fn anim_step(state: &Arc>) -> glib::Continue { } else { glib::Continue(true) } - } -impl Drop for Cursor { +impl Drop for BlinkCursor { fn drop(&mut self) { if let Some(timer_id) = self.state.borrow_mut().timer.take() { glib::source_remove(timer_id); @@ -264,6 +291,10 @@ impl Drop for Cursor { } } +pub trait CursorRedrawCb { + fn queue_redraw_cursor(&mut self); +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/file_browser.rs b/src/file_browser.rs new file mode 100644 index 0000000..cab0fc1 --- /dev/null +++ b/src/file_browser.rs @@ -0,0 +1,557 @@ +use std::cell::RefCell; +use std::cmp::Ordering; +use std::io; +use std::fs; +use std::fs::DirEntry; +use std::path::{Component, Path, PathBuf}; +use std::rc::Rc; +use std::ops::Deref; + +use gio; +use gio::prelude::*; +use gtk; +use gtk::MenuExt; +use gtk::prelude::*; + +use neovim_lib::{NeovimApi, NeovimApiAsync}; + +use misc::escape_filename; +use nvim::{ErrorReport, NeovimClient, NeovimRef}; +use shell; + +const ICON_FOLDER_CLOSED: &str = "folder-symbolic"; +const ICON_FOLDER_OPEN: &str = "folder-open-symbolic"; +const ICON_FILE: &str = "text-x-generic-symbolic"; + +struct Components { + dir_list_model: gtk::TreeStore, + dir_list: gtk::ComboBox, + context_menu: gtk::Menu, + show_hidden_checkbox: gtk::CheckMenuItem, + cd_action: gio::SimpleAction, +} + +struct State { + current_dir: String, + show_hidden: bool, + selected_path: Option, +} + +pub struct FileBrowserWidget { + store: gtk::TreeStore, + tree: gtk::TreeView, + widget: gtk::Box, + nvim: Option>, + comps: Components, + state: Rc>, +} + +impl Deref for FileBrowserWidget { + type Target = gtk::Box; + + fn deref(&self) -> >k::Box { + &self.widget + } +} + +#[derive(Copy, Clone, Debug)] +enum FileType { + File, + Dir, +} + +#[allow(dead_code)] +enum Column { + Filename, + Path, + FileType, + IconName, +} + +impl FileBrowserWidget { + pub fn new() -> Self { + let builder = gtk::Builder::new_from_string(include_str!("../resources/side-panel.ui")); + let widget: gtk::Box = builder.get_object("file_browser").unwrap(); + let tree: gtk::TreeView = builder.get_object("file_browser_tree_view").unwrap(); + let store: gtk::TreeStore = builder.get_object("file_browser_tree_store").unwrap(); + let dir_list_model: gtk::TreeStore = builder.get_object("dir_list_model").unwrap(); + let dir_list: gtk::ComboBox = builder.get_object("dir_list").unwrap(); + let context_menu: gtk::Menu = builder.get_object("file_browser_context_menu").unwrap(); + let show_hidden_checkbox: gtk::CheckMenuItem = builder + .get_object("file_browser_show_hidden_checkbox") + .unwrap(); + + let file_browser = FileBrowserWidget { + store, + tree, + widget, + nvim: None, + comps: Components { + dir_list_model, + dir_list, + context_menu, + show_hidden_checkbox, + cd_action: gio::SimpleAction::new("cd", None), + }, + state: Rc::new(RefCell::new(State { + current_dir: "".to_owned(), + show_hidden: false, + selected_path: None, + })), + }; + file_browser + } + + fn nvim(&self) -> Option { + self.nvim.as_ref().unwrap().nvim() + } + + pub fn init(&mut self, shell_state: &shell::State) { + // Initialize values. + let nvim = shell_state.nvim_clone(); + self.nvim = Some(nvim); + if let Some(dir) = get_current_dir(&mut self.nvim().unwrap()) { + update_dir_list(&dir, &self.comps.dir_list_model, &self.comps.dir_list); + self.state.borrow_mut().current_dir = dir; + } + + // Populate tree. + tree_reload(&self.store, &self.state.borrow()); + + let store = &self.store; + let state_ref = &self.state; + self.tree.connect_test_expand_row(clone!(store, state_ref => move |_, iter, _| { + store.set(&iter, &[Column::IconName as u32], &[&ICON_FOLDER_OPEN]); + // We cannot recursively populate all directories. Instead, we have prepared a single + // empty child entry for all non-empty directories, so the row will be expandable. Now, + // when a directory is expanded, populate its children. + let state = state_ref.borrow(); + if let Some(child) = store.iter_children(iter) { + let filename = store.get_value(&child, Column::Filename as i32); + if filename.get::<&str>().is_none() { + store.remove(&child); + let dir_value = store.get_value(&iter, Column::Path as i32); + if let Some(dir) = dir_value.get() { + populate_tree_nodes(&store, &state, dir, Some(iter)); + } + } else { + // This directory is already populated, i.e. it has been expanded and collapsed + // again. Rows further down the tree might have been silently collapsed without + // getting an event. Update their folder icon. + let mut tree_path = store.get_path(&child).unwrap(); + while let Some(iter) = store.get_iter(&tree_path) { + tree_path.next(); + let file_type = store + .get_value(&iter, Column::FileType as i32) + .get::(); + if file_type == Some(FileType::Dir as u8) { + store.set(&iter, &[Column::IconName as u32], &[&ICON_FOLDER_CLOSED]); + } + } + } + } + Inhibit(false) + })); + + self.tree.connect_row_collapsed(clone!(store => move |_, iter, _| { + store.set(&iter, &[Column::IconName as u32], &[&ICON_FOLDER_CLOSED]); + })); + + // Further initialization. + self.init_actions(); + self.init_subscriptions(shell_state); + self.connect_events(); + } + + fn init_actions(&self) { + let actions = gio::SimpleActionGroup::new(); + + let store = &self.store; + let state_ref = &self.state; + let nvim_ref = self.nvim.as_ref().unwrap(); + + let reload_action = gio::SimpleAction::new("reload", None); + reload_action.connect_activate(clone!(store, state_ref => move |_, _| { + tree_reload(&store, &state_ref.borrow()); + })); + actions.add_action(&reload_action); + + let cd_action = &self.comps.cd_action; + cd_action.connect_activate(clone!(state_ref, nvim_ref => move |_, _| { + let mut nvim = nvim_ref.nvim().unwrap(); + if let Some(ref path) = state_ref.borrow().selected_path { + nvim.set_current_dir_async(&path) + .cb(|r| r.report_err()) + .call(); + } + })); + actions.add_action(cd_action); + + self.comps + .context_menu + .insert_action_group("filebrowser", &actions); + } + + fn init_subscriptions(&self, shell_state: &shell::State) { + // Always set the current working directory as the root of the file browser. + let store = &self.store; + let state_ref = &self.state; + let dir_list_model = &self.comps.dir_list_model; + let dir_list = &self.comps.dir_list; + shell_state.subscribe( + "DirChanged", + &["getcwd()"], + clone!(store, state_ref, dir_list_model, dir_list => move |args| { + let dir = args.into_iter().next().unwrap(); + if dir != state_ref.borrow().current_dir { + state_ref.borrow_mut().current_dir = dir.to_owned(); + update_dir_list(&dir, &dir_list_model, &dir_list); + tree_reload(&store, &state_ref.borrow()); + } + }), + ); + + // Reveal the file of an entered buffer in the file browser and select the entry. + let tree = &self.tree; + let subscription = shell_state.subscribe( + "BufEnter", + &["getcwd()", "expand('%:p')"], + clone!(tree, store => move |args| { + let mut args_iter = args.into_iter(); + let dir = args_iter.next().unwrap(); + let file_path = args_iter.next().unwrap(); + let could_reveal = + if let Ok(rel_path) = Path::new(&file_path).strip_prefix(&Path::new(&dir)) { + reveal_path_in_tree(&store, &tree, &rel_path) + } else { + false + }; + if !could_reveal { + tree.get_selection().unselect_all(); + } + }), + ); + shell_state.run_now(&subscription); + } + + fn connect_events(&self) { + // Open file / go to dir, when user clicks on an entry. + let store = &self.store; + let state_ref = &self.state; + let nvim_ref = self.nvim.as_ref().unwrap(); + self.tree.connect_row_activated(clone!(store, state_ref, nvim_ref => move |tree, path, _| { + let iter = store.get_iter(path).unwrap(); + let file_type = store + .get_value(&iter, Column::FileType as i32) + .get::() + .unwrap(); + let file_path = store + .get_value(&iter, Column::Path as i32) + .get::() + .unwrap(); + if file_type == FileType::Dir as u8 { + let expanded = tree.row_expanded(path); + if expanded { + tree.collapse_row(path); + } else { + tree.expand_row(path, false); + } + } else { + // FileType::File + let cwd = &state_ref.borrow().current_dir; + let cwd = Path::new(cwd); + let file_path = if let Some(rel_path) = Path::new(&file_path) + .strip_prefix(&cwd) + .ok() + .and_then(|p| p.to_str()) + { + rel_path + } else { + &file_path + }; + let file_path = escape_filename(file_path); + nvim_ref.nvim().unwrap().command_async(&format!(":e {}", file_path)) + .cb(|r| r.report_err()) + .call(); + } + })); + + // Connect directory list. + let nvim_ref = self.nvim.as_ref().unwrap(); + self.comps.dir_list.connect_changed(clone!(nvim_ref, state_ref => move |dir_list| { + if let Some(iter) = dir_list.get_active_iter() { + let model = dir_list.get_model().unwrap(); + if let Some(dir) = model.get_value(&iter, 2).get::<&str>() { + if dir != state_ref.borrow().current_dir { + let mut nvim = nvim_ref.nvim().unwrap(); + nvim.set_current_dir_async(dir) + .cb(|r| r.report_err()) + .call(); + } + } + } + })); + + let context_menu = &self.comps.context_menu; + let cd_action = &self.comps.cd_action; + self.tree.connect_button_press_event( + clone!(store, state_ref, context_menu, cd_action => move |tree, ev_btn| { + // Open context menu on right click. + if ev_btn.get_button() == 3 { + context_menu.popup_at_pointer(&**ev_btn); + let (pos_x, pos_y) = ev_btn.get_position(); + let iter = tree + .get_path_at_pos(pos_x as i32, pos_y as i32) + .and_then(|(path, _, _, _)| path) + .and_then(|path| store.get_iter(&path)); + let file_type = iter + .as_ref() + .and_then(|iter| { + store + .get_value(&iter, Column::FileType as i32) + .get::() + }); + // Enable the "Go To Directory" action only if the user clicked on a folder. + cd_action.set_enabled(file_type == Some(FileType::Dir as u8)); + let path = iter + .and_then(|iter| { + store + .get_value(&iter, Column::Path as i32) + .get::() + }); + state_ref.borrow_mut().selected_path = path; + } + Inhibit(false) + }), + ); + + // Show / hide hidden files when corresponding menu item is toggled. + self.comps.show_hidden_checkbox.connect_toggled(clone!(state_ref, store => move |ev| { + let mut state = state_ref.borrow_mut(); + state.show_hidden = ev.get_active(); + tree_reload(&store, &state); + })); + } +} + +/// Compare function for dir entries. +/// +/// Sorts directories above files. +fn cmp_dirs_first(lhs: &DirEntry, rhs: &DirEntry) -> io::Result { + let lhs_metadata = fs::metadata(lhs.path())?; + let rhs_metadata = fs::metadata(rhs.path())?; + if lhs_metadata.is_dir() == rhs_metadata.is_dir() { + Ok(lhs.path() + .to_string_lossy() + .to_lowercase() + .cmp(&rhs.path().to_string_lossy().to_lowercase())) + } else { + if lhs_metadata.is_dir() { + Ok(Ordering::Less) + } else { + Ok(Ordering::Greater) + } + } +} + +/// Clears an repopulate the entire tree. +fn tree_reload(store: >k::TreeStore, state: &State) { + let dir = &state.current_dir; + store.clear(); + populate_tree_nodes(store, state, dir, None); +} + +/// Updates the dirctory list on top of the file browser. +/// +/// The list represents the path the the current working directory. If the new cwd is a parent of +/// the old one, the list is kept and only the active entry is updated. Otherwise, the list is +/// replaced with the new path and the last entry is marked active. +fn update_dir_list(dir: &str, dir_list_model: >k::TreeStore, dir_list: >k::ComboBox) { + // The current working directory path. + let complete_path = Path::new(dir); + let mut path = PathBuf::new(); + let mut components = complete_path.components(); + let mut next = components.next(); + + // Iterator over existing dir_list model. + let mut dir_list_iter = dir_list_model.get_iter_first(); + + // Whether existing entries up to the current position of dir_list_iter are a prefix of the + // new current working directory path. + let mut is_prefix = true; + + // Iterate over components of the cwd. Simultaneously move dir_list_iter forward. + while let Some(dir) = next { + next = components.next(); + let dir_name = &*dir.as_os_str().to_string_lossy(); + // Assemble path up to current component. + path.push(Path::new(&dir)); + let path_str = path.to_str().unwrap_or_else(|| { + error!( + "Could not convert path to string: {}\n + Directory chooser will not work for that entry.", + path.to_string_lossy() + ); + "" + }); + // Use the current entry of dir_list, if any, otherwise append a new one. + let current_iter = dir_list_iter.unwrap_or_else(|| dir_list_model.append(None)); + // Check if the current entry is still part of the new cwd. + if is_prefix && dir_list_model.get_value(¤t_iter, 0).get::<&str>() != Some(&dir_name) + { + is_prefix = false; + } + if next.is_some() { + // Update dir_list entry. + dir_list_model.set( + ¤t_iter, + &[0, 1, 2], + &[&dir_name, &ICON_FOLDER_CLOSED, &path_str], + ); + } else { + // We reached the last component of the new cwd path. Set the active entry of dir_list + // to this one. + dir_list_model.set( + ¤t_iter, + &[0, 1, 2], + &[&dir_name, &ICON_FOLDER_OPEN, &path_str], + ); + dir_list.set_active_iter(¤t_iter); + }; + // Advance dir_list_iter. + dir_list_iter = if dir_list_model.iter_next(¤t_iter) { + Some(current_iter) + } else { + None + } + } + // We updated the dir list to the point of the current working directory. + if let Some(iter) = dir_list_iter { + if is_prefix { + // If we didn't change any entries to this point and the list contains further entries, + // the remaining ones are subdirectories of the cwd and we keep them. + loop { + dir_list_model.set(&iter, &[1], &[&ICON_FOLDER_CLOSED]); + if !dir_list_model.iter_next(&iter) { + break; + } + } + } else { + // If we needed to change entries, the following ones are not directories under the + // cwd and we clear them. + while dir_list_model.remove(&iter) {} + } + } +} + +/// Populates one level, i.e. one directory of the file browser tree. +fn populate_tree_nodes( + store: >k::TreeStore, + state: &State, + dir: &str, + parent: Option<>k::TreeIter>, +) { + let path = Path::new(dir); + let read_dir = match path.read_dir() { + Ok(read_dir) => read_dir, + Err(err) => { + error!("Couldn't populate tree: {}", err); + return; + } + }; + let iter = read_dir.filter_map(Result::ok); + let mut entries: Vec = if state.show_hidden { + iter.collect() + } else { + iter.filter(|entry| !entry.file_name().to_string_lossy().starts_with(".")) + .filter(|entry| !entry.file_name().to_string_lossy().ends_with("~")) + .collect() + }; + entries.sort_unstable_by(|lhs, rhs| cmp_dirs_first(lhs, rhs).unwrap_or(Ordering::Equal)); + for entry in entries { + let path = if let Some(path) = entry.path().to_str() { + path.to_owned() + } else { + // Skip paths that contain invalid unicode. + continue; + }; + let filename = entry.file_name().to_str().unwrap().to_owned(); + let file_type = if let Ok(metadata) = fs::metadata(entry.path()) { + let file_type = metadata.file_type(); + if file_type.is_dir() { + FileType::Dir + } else if file_type.is_file() { + FileType::File + } else { + continue; + } + } else { + // In case of invalid symlinks, we cannot obtain metadata. + continue; + }; + let icon = match file_type { + FileType::Dir => ICON_FOLDER_CLOSED, + FileType::File => ICON_FILE, + }; + // When we get until here, we want to show the entry. Append it to the tree. + let iter = store.append(parent); + store.set( + &iter, + &[0, 1, 2, 3], + &[&filename, &path, &(file_type as u8), &icon], + ); + // For directories, check whether the directory is empty. If not, append a single empty + // entry, so the expand arrow is shown. Its contents are dynamically populated when + // expanded (see `init`). + if let FileType::Dir = file_type { + let not_empty = if let Ok(mut dir) = entry.path().read_dir() { + dir.next().is_some() + } else { + false + }; + if not_empty { + let iter = store.append(&iter); + store.set(&iter, &[], &[]); + } + } + } +} + +fn get_current_dir(nvim: &mut NeovimRef) -> Option { + match nvim.eval("getcwd()") { + Ok(cwd) => cwd.as_str().map(|s| s.to_owned()), + Err(err) => { + error!("Couldn't get cwd: {}", err); + None + } + } +} + +/// Reveals and selects the given file in the file browser. +/// +/// Returns `true` if the file could be successfully revealed. +fn reveal_path_in_tree(store: >k::TreeStore, tree: >k::TreeView, rel_file_path: &Path) -> bool { + let mut tree_path = gtk::TreePath::new(); + 'components: for component in rel_file_path.components() { + if let Component::Normal(component) = component { + tree_path.down(); + while let Some(iter) = store.get_iter(&tree_path) { + let entry_value = store.get_value(&iter, Column::Filename as i32); + let entry = entry_value.get::<&str>().unwrap(); + if component == entry { + tree.expand_row(&tree_path, false); + continue 'components; + } + tree_path.next(); + } + return false; + } else { + return false; + } + } + if tree_path.get_depth() == 0 { + return false; + } + tree.set_cursor(&tree_path, None, false); + true +} diff --git a/src/input.rs b/src/input.rs index b650258..e0785f1 100644 --- a/src/input.rs +++ b/src/input.rs @@ -5,8 +5,6 @@ use gdk::EventKey; use phf; use neovim_lib::{Neovim, NeovimApi}; -use std::ascii::AsciiExt; - include!(concat!(env!("OUT_DIR"), "/key_map_table.rs")); diff --git a/src/main.rs b/src/main.rs index 2faa62d..1477aeb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,5 @@ +#![windows_subsystem = "windows"] + extern crate cairo; extern crate env_logger; extern crate gdk; @@ -11,13 +13,19 @@ extern crate gtk; extern crate gtk_sys; extern crate htmlescape; #[macro_use] +extern crate lazy_static; +#[macro_use] extern crate log; extern crate neovim_lib; extern crate pango; extern crate pango_cairo_sys; extern crate pango_sys; extern crate pangocairo; +extern crate percent_encoding; extern crate phf; +extern crate rmpv; +extern crate regex; +extern crate unicode_width; extern crate serde; #[macro_use] @@ -43,11 +51,15 @@ mod shell; mod input; mod settings; mod cursor; +mod cmd_line; mod shell_dlg; mod popup_menu; mod project; mod tabline; mod error; +mod file_browser; +mod subscriptions; +mod misc; use std::env; use std::time::Duration; @@ -96,21 +108,23 @@ fn main() { } fn open(app: >k::Application, files: &[gio::File], _: &str) { - for f in files { - let mut ui = Ui::new(ShellOptions::new( - nvim_bin_path(std::env::args()), - f.get_path().and_then(|p| p.to_str().map(str::to_owned)), - nvim_timeout(std::env::args()), - )); + let files_list: Vec = files + .into_iter() + .filter_map(|f| f.get_path()?.to_str().map(str::to_owned)) + .collect(); + let mut ui = Ui::new(ShellOptions::new( + nvim_bin_path(std::env::args()), + files_list, + nvim_timeout(std::env::args()), + )); - ui.init(app, !nvim_disable_win_state(std::env::args())); - } + ui.init(app, !nvim_disable_win_state(std::env::args())); } fn activate(app: >k::Application) { let mut ui = Ui::new(ShellOptions::new( nvim_bin_path(std::env::args()), - None, + Vec::new(), nvim_timeout(std::env::args()), )); diff --git a/src/misc.rs b/src/misc.rs new file mode 100644 index 0000000..2c3c768 --- /dev/null +++ b/src/misc.rs @@ -0,0 +1,38 @@ +use std::borrow::Cow; + +use regex::Regex; +use percent_encoding::percent_decode; + +/// Escape special ASCII characters with a backslash. +pub fn escape_filename<'t>(filename: &'t str) -> Cow<'t, str> { + lazy_static! { + static ref SPECIAL_CHARS: Regex = if cfg!(target_os = "windows") { + // On Windows, don't escape `:` and `\`, as these are valid components of the path. + Regex::new(r"[[:ascii:]&&[^0-9a-zA-Z._:\\-]]").unwrap() + } else { + // Similarly, don't escape `/` on other platforms. + Regex::new(r"[[:ascii:]&&[^0-9a-zA-Z._/-]]").unwrap() + }; + } + SPECIAL_CHARS.replace_all(&*filename, r"\$0") +} + +/// Decode a file URI. +/// +/// - On UNIX: `file:///path/to/a%20file.ext` -> `/path/to/a file.ext` +/// - On Windows: `file:///C:/path/to/a%20file.ext` -> `C:\path\to\a file.ext` +pub fn decode_uri(uri: &str) -> Option { + let path = match uri.split_at(8) { + ("file:///", path) => path, + _ => return None, + }; + let path = percent_decode(path.as_bytes()).decode_utf8().ok()?; + if cfg!(target_os = "windows") { + lazy_static! { + static ref SLASH: Regex = Regex::new(r"/").unwrap(); + } + Some(String::from(SLASH.replace_all(&*path, r"\"))) + } else { + Some("/".to_owned() + &path) + } +} diff --git a/src/nvim/client.rs b/src/nvim/client.rs index a70b98b..5e34554 100644 --- a/src/nvim/client.rs +++ b/src/nvim/client.rs @@ -36,9 +36,7 @@ impl<'a> NeovimRef<'a> { pub fn non_blocked(mut self) -> Option { self.get_mode().ok_and_report().and_then(|mode| { mode.iter() - .find(|kv| { - kv.0.as_str().map(|key| key == "blocking").unwrap_or(false) - }) + .find(|kv| kv.0.as_str().map(|key| key == "blocking").unwrap_or(false)) .map(|kv| kv.1.as_bool().unwrap_or(false)) .and_then(|block| if block { None } else { Some(self) }) }) @@ -71,7 +69,9 @@ pub struct NeovimClientAsync { impl NeovimClientAsync { fn new() -> Self { - NeovimClientAsync { nvim: Arc::new(Mutex::new(None)) } + NeovimClientAsync { + nvim: Arc::new(Mutex::new(None)), + } } pub fn borrow(&self) -> Option { @@ -81,7 +81,9 @@ impl NeovimClientAsync { impl Clone for NeovimClientAsync { fn clone(&self) -> Self { - NeovimClientAsync { nvim: self.nvim.clone() } + NeovimClientAsync { + nvim: self.nvim.clone(), + } } } @@ -147,9 +149,9 @@ impl NeovimClient { pub fn nvim(&self) -> Option { let nvim = self.nvim.borrow_mut(); if nvim.is_some() { - Some(NeovimRef::from_nvim( - RefMut::map(nvim, |n| n.as_mut().unwrap()), - )) + Some(NeovimRef::from_nvim(RefMut::map(nvim, |n| { + n.as_mut().unwrap() + }))) } else { self.nvim_async.borrow() } diff --git a/src/nvim/ext.rs b/src/nvim/ext.rs index 8dc7283..f50961c 100644 --- a/src/nvim/ext.rs +++ b/src/nvim/ext.rs @@ -1,4 +1,3 @@ - use std::result; use neovim_lib::CallError; diff --git a/src/nvim/handler.rs b/src/nvim/handler.rs index fe25085..4e471ec 100644 --- a/src/nvim/handler.rs +++ b/src/nvim/handler.rs @@ -1,5 +1,5 @@ use std::result; -use std::sync::{Arc, mpsc}; +use std::sync::{mpsc, Arc}; use neovim_lib::{Handler, Value}; @@ -9,53 +9,63 @@ use glib; use super::repaint_mode::RepaintMode; use super::redraw_handler; -use super::redraw_handler::RedrawEvents; pub struct NvimHandler { shell: Arc>, + + delayed_redraw_event_id: Arc>>, } impl NvimHandler { pub fn new(shell: Arc>) -> NvimHandler { - NvimHandler { shell: shell } + NvimHandler { + shell, + delayed_redraw_event_id: Arc::new(UiMutex::new(None)), + } } - fn nvim_cb(&self, method: &str, params: Vec) { + pub fn schedule_redraw_event(&self, event: Value) { + let shell = self.shell.clone(); + let delayed_redraw_event_id = self.delayed_redraw_event_id.clone(); + + glib::idle_add(move || { + let id = Some(glib::timeout_add( + 250, + clone!(shell, event, delayed_redraw_event_id => move || { + delayed_redraw_event_id.replace(None); + + if let Err(msg) = call_redraw_handler(vec![event.clone()], &shell) { + error!("Error call function: {}", msg); + } + + glib::Continue(false) + }), + )); + + delayed_redraw_event_id.replace(id); + + glib::Continue(false) + }); + } + + pub fn remove_scheduled_redraw_event(&self) { + let delayed_redraw_event_id = self.delayed_redraw_event_id.clone(); + glib::idle_add(move || { + let id = delayed_redraw_event_id.replace(None); + if let Some(ev_id) = id { + glib::source_remove(ev_id); + } + + glib::Continue(false) + }); + } + + fn nvim_cb(&self, method: &str, mut params: Vec) { match method { "redraw" => { - self.safe_call(move |ui| { - let ui = &mut ui.borrow_mut(); - let mut repaint_mode = RepaintMode::Nothing; + redraw_handler::remove_or_delay_uneeded_events(self, &mut params); - for ev in params { - if let Value::Array(ev_args) = ev { - let mut args_iter = ev_args.into_iter(); - let ev_name = args_iter.next(); - if let Some(ev_name) = ev_name { - if let Some(ev_name) = ev_name.as_str() { - for local_args in args_iter { - let args = match local_args { - Value::Array(ar) => ar, - _ => vec![], - }; - let call_reapint_mode = - redraw_handler::call(ui, &ev_name, &args)?; - repaint_mode = repaint_mode.join(call_reapint_mode); - } - } else { - error!("Unsupported event"); - } - } else { - error!("Event name does not exists"); - } - } else { - error!("Unsupported event type {:?}", ev); - } - } - - ui.on_redraw(&repaint_mode); - Ok(()) - }); + self.safe_call(move |ui| call_redraw_handler(params, ui)); } "Gui" => { if !params.is_empty() { @@ -67,8 +77,10 @@ impl NvimHandler { let ui = &mut ui.borrow_mut(); redraw_handler::call_gui_event( ui, - ev_name.as_str().ok_or_else(|| "Event name does not exists")?, - &args, + ev_name + .as_str() + .ok_or_else(|| "Event name does not exists")?, + args, )?; ui.on_redraw(&RepaintMode::All); Ok(()) @@ -83,13 +95,19 @@ impl NvimHandler { error!("Unsupported event {:?}", params); } } + "subscription" => { + self.safe_call(move |ui| { + let ui = &ui.borrow(); + ui.notify(params) + }); + } _ => { error!("Notification {}({:?})", method, params); } } } - fn nvim_cb_req (&self, method: &str, params: Vec) -> result::Result { + fn nvim_cb_req(&self, method: &str, params: Vec) -> result::Result { match method { "Gui" => { if !params.is_empty() { @@ -99,11 +117,15 @@ impl NvimHandler { let args = params_iter.collect(); let (sender, receiver) = mpsc::channel(); self.safe_call(move |ui| { - sender.send(redraw_handler::call_gui_request( - &ui.clone(), - req_name.as_str().ok_or_else(|| "Event name does not exists")?, - &args, - )).unwrap(); + sender + .send(redraw_handler::call_gui_request( + &ui.clone(), + req_name + .as_str() + .ok_or_else(|| "Event name does not exists")?, + &args, + )) + .unwrap(); { let ui = &mut ui.borrow_mut(); ui.on_redraw(&RepaintMode::All); @@ -123,29 +145,71 @@ impl NvimHandler { error!("Unsupported request {:?}", params); Err(Value::Nil) } - }, + } _ => { error!("Request {}({:?})", method, params); Err(Value::Nil) } } } - + fn safe_call(&self, cb: F) where F: FnOnce(&Arc>) -> result::Result<(), String> + 'static + Send, { - let mut cb = Some(cb); - let shell = self.shell.clone(); - glib::idle_add(move || { - if let Err(msg) = cb.take().unwrap()(&shell) { - error!("Error call function: {}", msg); - } - glib::Continue(false) - }); + safe_call(self.shell.clone(), cb); } } +fn call_redraw_handler( + params: Vec, + ui: &Arc>, +) -> result::Result<(), String> { + let ui = &mut ui.borrow_mut(); + let mut repaint_mode = RepaintMode::Nothing; + + for ev in params { + if let Value::Array(ev_args) = ev { + let mut args_iter = ev_args.into_iter(); + let ev_name = args_iter.next(); + if let Some(ev_name) = ev_name { + if let Some(ev_name) = ev_name.as_str() { + for local_args in args_iter { + let args = match local_args { + Value::Array(ar) => ar, + _ => vec![], + }; + let call_reapint_mode = redraw_handler::call(ui, &ev_name, args)?; + repaint_mode = repaint_mode.join(call_reapint_mode); + } + } else { + error!("Unsupported event"); + } + } else { + error!("Event name does not exists"); + } + } else { + error!("Unsupported event type {:?}", ev); + } + } + + ui.on_redraw(&repaint_mode); + Ok(()) +} + +fn safe_call(shell: Arc>, cb: F) +where + F: FnOnce(&Arc>) -> result::Result<(), String> + 'static + Send, +{ + let mut cb = Some(cb); + glib::idle_add(move || { + if let Err(msg) = cb.take().unwrap()(&shell) { + error!("Error call function: {}", msg); + } + glib::Continue(false) + }); +} + impl Handler for NvimHandler { fn handle_notify(&mut self, name: &str, args: Vec) { self.nvim_cb(name, args); diff --git a/src/nvim/mod.rs b/src/nvim/mod.rs index 925db89..5ec8c92 100644 --- a/src/nvim/mod.rs +++ b/src/nvim/mod.rs @@ -1,4 +1,3 @@ - mod client; mod handler; mod mode_info; @@ -6,24 +5,23 @@ mod redraw_handler; mod repaint_mode; mod ext; -pub use self::redraw_handler::{RedrawEvents, GuiApi, CompleteItem}; +pub use self::redraw_handler::CompleteItem; pub use self::repaint_mode::RepaintMode; pub use self::client::{NeovimClient, NeovimClientAsync, NeovimRef}; -pub use self::mode_info::{ModeInfo, CursorShape}; +pub use self::mode_info::{CursorShape, ModeInfo}; pub use self::ext::ErrorReport; +pub use self::handler::NvimHandler; use std::error; use std::fmt; use std::env; -use std::process::{Stdio, Command}; +use std::process::{Command, Stdio}; use std::result; -use std::sync::Arc; use std::time::Duration; -use neovim_lib::{Neovim, NeovimApi, Session, UiAttachOptions}; +use neovim_lib::{Neovim, NeovimApi, NeovimApiAsync, Session, UiAttachOptions}; -use ui::UiMutex; -use shell; +use misc::escape_filename; use nvim_config::NvimConfig; #[derive(Debug)] @@ -78,8 +76,14 @@ impl error::Error for NvimInitError { } } +#[cfg(target_os = "windows")] +fn set_windows_creation_flags(cmd: &mut Command) { + use std::os::windows::process::CommandExt; + cmd.creation_flags(0x08000000); // CREATE_NO_WINDOW +} + pub fn start( - shell: Arc>, + handler: NvimHandler, nvim_bin_path: Option<&String>, timeout: Option, ) -> result::Result { @@ -99,15 +103,15 @@ pub fn start( .arg("let g:GtkGuiLoaded = 1") .stderr(Stdio::inherit()); + #[cfg(target_os = "windows")] + set_windows_creation_flags(&mut cmd); + if let Ok(runtime_path) = env::var("NVIM_GTK_RUNTIME_PATH") { - cmd.arg("--cmd").arg( - format!("let &rtp.=',{}'", runtime_path), - ); + cmd.arg("--cmd") + .arg(format!("let &rtp.=',{}'", runtime_path)); } else if let Some(prefix) = option_env!("PREFIX") { - cmd.arg("--cmd").arg(format!( - "let &rtp.=',{}/share/nvim-gtk/runtime'", - prefix - )); + cmd.arg("--cmd") + .arg(format!("let &rtp.=',{}/share/nvim-gtk/runtime'", prefix)); } else { cmd.arg("--cmd").arg("let &rtp.=',runtime'"); } @@ -129,38 +133,46 @@ pub fn start( let mut nvim = Neovim::new(session); - nvim.session.start_event_loop_handler( - handler::NvimHandler::new(shell), - ); + nvim.session.start_event_loop_handler(handler); Ok(nvim) } pub fn post_start_init( nvim: NeovimClientAsync, - open_path: Option<&String>, + open_paths: Vec, cols: u64, rows: u64, ) -> result::Result<(), NvimInitError> { - let mut opts = UiAttachOptions::new(); - opts.set_popupmenu_external(true); - opts.set_tabline_external(true); nvim.borrow() .unwrap() - .ui_attach(cols, rows, &opts) + .ui_attach( + cols, + rows, + UiAttachOptions::new() + .set_popupmenu_external(true) + .set_tabline_external(true), + ) .map_err(NvimInitError::new_post_init)?; + nvim.borrow() .unwrap() .command("runtime! ginit.vim") .map_err(NvimInitError::new_post_init)?; - if let Some(path) = open_path { + if !open_paths.is_empty() { + let command = open_paths + .iter() + .fold(":ar".to_owned(), |command, filename| { + let filename = escape_filename(filename); + command + " " + &filename + }); nvim.borrow() .unwrap() - .command(&format!("e {}", path)) - .map_err(NvimInitError::new_post_init)?; + .command_async(&command) + .cb(|r| r.report_err()) + .call(); } Ok(()) } - diff --git a/src/nvim/mode_info.rs b/src/nvim/mode_info.rs index 4d0de6f..7515d7e 100644 --- a/src/nvim/mode_info.rs +++ b/src/nvim/mode_info.rs @@ -12,9 +12,9 @@ pub enum CursorShape { impl CursorShape { fn new(shape_code: &Value) -> Result { - let str_code = shape_code.as_str().ok_or_else(|| { - "Can't convert cursor shape to string".to_owned() - })?; + let str_code = shape_code + .as_str() + .ok_or_else(|| "Can't convert cursor shape to string".to_owned())?; Ok(match str_code { "block" => CursorShape::Block, diff --git a/src/nvim/redraw_handler.rs b/src/nvim/redraw_handler.rs index 9f7122b..865f91b 100644 --- a/src/nvim/redraw_handler.rs +++ b/src/nvim/redraw_handler.rs @@ -1,7 +1,7 @@ use std::result; use std::sync::Arc; -use neovim_lib::{Value, UiOption}; +use neovim_lib::{UiOption, Value}; use neovim_lib::neovim_api::Tabpage; use ui::UiMutex; @@ -9,69 +9,11 @@ use shell; use gtk::ClipboardExt; use value::ValueMapExt; +use rmpv; use super::repaint_mode::RepaintMode; use super::mode_info::ModeInfo; - -pub trait RedrawEvents { - fn on_cursor_goto(&mut self, row: u64, col: u64) -> RepaintMode; - - fn on_put(&mut self, text: &str) -> RepaintMode; - - fn on_clear(&mut self) -> RepaintMode; - - fn on_resize(&mut self, columns: u64, rows: u64) -> RepaintMode; - - fn on_redraw(&mut self, mode: &RepaintMode); - - fn on_highlight_set(&mut self, attrs: &[(Value, Value)]) -> RepaintMode; - - fn on_eol_clear(&mut self) -> RepaintMode; - - fn on_set_scroll_region(&mut self, top: u64, bot: u64, left: u64, right: u64) -> RepaintMode; - - fn on_scroll(&mut self, count: i64) -> RepaintMode; - - fn on_update_bg(&mut self, bg: i64) -> RepaintMode; - - fn on_update_fg(&mut self, fg: i64) -> RepaintMode; - - fn on_update_sp(&mut self, sp: i64) -> RepaintMode; - - fn on_mode_change(&mut self, mode: &str, idx: u64) -> RepaintMode; - - fn on_mouse(&mut self, on: bool) -> RepaintMode; - - fn on_busy(&mut self, busy: bool) -> RepaintMode; - - fn popupmenu_show( - &mut self, - menu: &[CompleteItem], - selected: i64, - row: u64, - col: u64, - ) -> RepaintMode; - - fn popupmenu_hide(&mut self) -> RepaintMode; - - fn popupmenu_select(&mut self, selected: i64) -> RepaintMode; - - fn tabline_update( - &mut self, - selected: Tabpage, - tabs: Vec<(Tabpage, Option)>, - ) -> RepaintMode; - - fn mode_info_set( - &mut self, - cursor_style_enabled: bool, - mode_info: Vec, - ) -> RepaintMode; -} - -pub trait GuiApi { - fn set_font(&mut self, font_desc: &str); -} +use super::handler::NvimHandler; macro_rules! try_str { ($exp:expr) => ($exp.as_str().ok_or_else(|| "Can't convert argument to string".to_owned())?) @@ -106,45 +48,75 @@ macro_rules! map_array { ); } +macro_rules! try_arg { + ($value:expr, bool) => (try_bool!($value)); + ($value:expr, uint) => (try_uint!($value)); + ($value:expr, int) => (try_int!($value)); + ($value:expr, str) => ( + match $value { + Value::String(s) => { + if let Some(s) = s.into_str() { + Ok(s) + } else { + Err("Can't convert to utf8 string".to_owned()) + } + } + _ => Err("Can't convert to string".to_owned()), + }?); + ($value:expr, ext) => (rmpv::ext::from_value($value).map_err(|e| e.to_string())?); +} + +macro_rules! call { + ($s:ident -> $c:ident ($args:ident : $($arg_type:ident),+ )) => ( + { + let mut iter = $args.into_iter(); + $s.$c($( + try_arg!(iter.next() + .ok_or_else(|| format!("No such argument for {}", stringify!($c)))?, + $arg_type + ) + ),+ ) + } + ) +} pub fn call_gui_event( ui: &mut shell::State, method: &str, - args: &Vec, + args: Vec, ) -> result::Result<(), String> { match method { - "Font" => ui.set_font(try_str!(args[0])), - "Clipboard" => { - match try_str!(args[0]) { - "Set" => { - match try_str!(args[1]) { - "*" => ui.clipboard_primary_set(try_str!(args[2])), - _ => ui.clipboard_clipboard_set(try_str!(args[2])), - } - }, - opt => error!("Unknown option {}", opt), - } + "Font" => call!(ui->set_font(args: str)), + "Clipboard" => match try_str!(args[0]) { + "Set" => match try_str!(args[1]) { + "*" => ui.clipboard_primary_set(try_str!(args[2])), + _ => ui.clipboard_clipboard_set(try_str!(args[2])), + }, + opt => error!("Unknown option {}", opt), }, - "Option" => { - match try_str!(args[0]) { - "Popupmenu" => { - ui.nvim() - .ok_or_else(|| "Nvim not initialized".to_owned()) - .and_then(|mut nvim| { - nvim.set_option(UiOption::ExtPopupmenu(try_uint!(args[1]) == 1)) - .map_err(|e| e.to_string()) - })? - } - "Tabline" => { - ui.nvim() - .ok_or_else(|| "Nvim not initialized".to_owned()) - .and_then(|mut nvim| { - nvim.set_option(UiOption::ExtTabline(try_uint!(args[1]) == 1)) - .map_err(|e| e.to_string()) - })? - } - opt => error!("Unknown option {}", opt), - } + "Option" => match try_str!(args[0]) { + "Popupmenu" => ui.nvim() + .ok_or_else(|| "Nvim not initialized".to_owned()) + .and_then(|mut nvim| { + nvim.set_option(UiOption::ExtPopupmenu(try_uint!(args[1]) == 1)) + .map_err(|e| e.to_string()) + })?, + "Tabline" => ui.nvim() + .ok_or_else(|| "Nvim not initialized".to_owned()) + .and_then(|mut nvim| { + nvim.set_option(UiOption::ExtTabline(try_uint!(args[1]) == 1)) + .map_err(|e| e.to_string()) + })?, + "Cmdline" => ui.nvim() + .ok_or_else(|| "Nvim not initialized".to_owned()) + .and_then(|mut nvim| { + nvim.set_option(UiOption::ExtCmdline(try_uint!(args[1]) == 1)) + .map_err(|e| e.to_string()) + })?, + opt => error!("Unknown option {}", opt), + }, + "Command" => { + ui.on_command(args); } _ => return Err(format!("Unsupported event {}({:?})", method, args)), } @@ -171,61 +143,56 @@ pub fn call_gui_request( } }; let t = clipboard.wait_for_text().unwrap_or_else(|| String::new()); - Ok(Value::Array(t.split("\n").map(|s| s.into()).collect::>())) - }, + Ok(Value::Array( + t.split("\n").map(|s| s.into()).collect::>(), + )) + } opt => { error!("Unknown option {}", opt); Err(Value::Nil) - }, + } } - }, - _ => Err(Value::String(format!("Unsupported request {}({:?})", method, args).into())), + } + _ => Err(Value::String( + format!("Unsupported request {}({:?})", method, args).into(), + )), } } pub fn call( ui: &mut shell::State, method: &str, - args: &[Value], + args: Vec, ) -> result::Result { let repaint_mode = match method { - "cursor_goto" => ui.on_cursor_goto(try_uint!(args[0]), try_uint!(args[1])), - "put" => ui.on_put(try_str!(args[0])), + "cursor_goto" => call!(ui->on_cursor_goto(args: uint, uint)), + "put" => call!(ui->on_put(args: str)), "clear" => ui.on_clear(), - "resize" => ui.on_resize(try_uint!(args[0]), try_uint!(args[1])), + "resize" => call!(ui->on_resize(args: uint, uint)), "highlight_set" => { - if let Value::Map(ref attrs) = args[0] { - ui.on_highlight_set(attrs); - } else { - panic!("Supports only map value as argument"); - } + call!(ui->on_highlight_set(args: ext)); RepaintMode::Nothing } "eol_clear" => ui.on_eol_clear(), "set_scroll_region" => { - ui.on_set_scroll_region( - try_uint!(args[0]), - try_uint!(args[1]), - try_uint!(args[2]), - try_uint!(args[3]), - ); + call!(ui->on_set_scroll_region(args: uint, uint, uint, uint)); RepaintMode::Nothing } - "scroll" => ui.on_scroll(try_int!(args[0])), - "update_bg" => ui.on_update_bg(try_int!(args[0])), - "update_fg" => ui.on_update_fg(try_int!(args[0])), - "update_sp" => ui.on_update_sp(try_int!(args[0])), - "mode_change" => ui.on_mode_change(try_str!(args[0]), try_uint!(args[1])), + "scroll" => call!(ui->on_scroll(args: int)), + "update_bg" => call!(ui->on_update_bg(args: int)), + "update_fg" => call!(ui->on_update_fg(args: int)), + "update_sp" => call!(ui->on_update_sp(args: int)), + "mode_change" => call!(ui->on_mode_change(args: str, uint)), "mouse_on" => ui.on_mouse(true), "mouse_off" => ui.on_mouse(false), "busy_start" => ui.on_busy(true), "busy_stop" => ui.on_busy(false), "popupmenu_show" => { - let menu_items = map_array!(args[0], "Error get menu list array", |item| { - map_array!(item, "Error get menu item array", |col| { - col.as_str().ok_or("Error get menu column") - }) - })?; + let menu_items = map_array!(args[0], "Error get menu list array", |item| map_array!( + item, + "Error get menu item array", + |col| col.as_str().ok_or("Error get menu column") + ))?; ui.popupmenu_show( &CompleteItem::map(&menu_items), @@ -235,16 +202,16 @@ pub fn call( ) } "popupmenu_hide" => ui.popupmenu_hide(), - "popupmenu_select" => ui.popupmenu_select(try_int!(args[0])), + "popupmenu_select" => call!(ui->popupmenu_select(args: int)), "tabline_update" => { let tabs_out = map_array!(args[1], "Error get tabline list".to_owned(), |tab| { tab.as_map() .ok_or_else(|| "Error get map for tab".to_owned()) .and_then(|tab_map| tab_map.to_attrs_map()) .map(|tab_attrs| { - let name_attr = tab_attrs.get("name").and_then( - |n| n.as_str().map(|s| s.to_owned()), - ); + let name_attr = tab_attrs + .get("name") + .and_then(|n| n.as_str().map(|s| s.to_owned())); let tab_attr = tab_attrs .get("tab") .map(|&tab_id| Tabpage::new(tab_id.clone())) @@ -259,16 +226,21 @@ pub fn call( let mode_info = map_array!( args[1], "Error get array key value for mode_info".to_owned(), - |mi| { - mi.as_map() - .ok_or_else(|| "Erro get map for mode_info".to_owned()) - .and_then(|mi_map| ModeInfo::new(mi_map)) - } + |mi| mi.as_map() + .ok_or_else(|| "Erro get map for mode_info".to_owned()) + .and_then(|mi_map| ModeInfo::new(mi_map)) )?; ui.mode_info_set(try_bool!(args[0]), mode_info) } + "cmdline_show" => call!(ui->cmdline_show(args: ext, uint, str, str, uint, uint)), + "cmdline_block_show" => call!(ui->cmdline_block_show(args: ext)), + "cmdline_block_append" => call!(ui->cmdline_block_append(args: ext)), + "cmdline_hide" => call!(ui->cmdline_hide(args: uint)), + "cmdline_block_hide" => ui.cmdline_block_hide(), + "cmdline_pos" => call!(ui->cmdline_pos(args: uint, uint)), + "cmdline_special_char" => call!(ui->cmdline_special_char(args: str, bool, uint)), _ => { - println!("Event {}({:?})", method, args); + warn!("Event {}({:?})", method, args); RepaintMode::Nothing } }; @@ -276,6 +248,49 @@ pub fn call( Ok(repaint_mode) } +// Here two cases processed: +// +// 1. menu content update call popupmenu_hide followed by popupmenu_show in same batch +// this generates unneded hide event +// so in case we get both events, just romove one +// +// 2. hide event postpone in case show event come bit later +// but in new event batch +pub fn remove_or_delay_uneeded_events(handler: &NvimHandler, params: &mut Vec) { + let mut show_popup_finded = false; + let mut to_remove = Vec::new(); + let mut delayed_hide_event = None; + + for (idx, val) in params.iter().enumerate().rev() { + if let Some(args) = val.as_array() { + match args[0].as_str() { + Some("popupmenu_show") => { + show_popup_finded = true; + handler.remove_scheduled_redraw_event(); + } + Some("popupmenu_hide") if !show_popup_finded && delayed_hide_event.is_none() => { + to_remove.push(idx); + delayed_hide_event = Some(idx); + handler.remove_scheduled_redraw_event(); + } + Some("popupmenu_hide") => { + to_remove.push(idx); + } + _ => (), + } + } + } + + to_remove.iter().for_each(|&idx| { + let ev = params.remove(idx); + if let Some(delayed_hide_event_idx) = delayed_hide_event { + if delayed_hide_event_idx == idx { + handler.schedule_redraw_event(ev); + } + } + }); +} + pub struct CompleteItem<'a> { pub word: &'a str, pub kind: &'a str, @@ -286,13 +301,11 @@ pub struct CompleteItem<'a> { impl<'a> CompleteItem<'a> { fn map(menu: &'a [Vec<&str>]) -> Vec { menu.iter() - .map(|menu| { - CompleteItem { - word: menu[0], - kind: menu[1], - menu: menu[2], - info: menu[3], - } + .map(|menu| CompleteItem { + word: menu[0], + kind: menu[1], + menu: menu[2], + info: menu[3], }) .collect() } diff --git a/src/nvim/repaint_mode.rs b/src/nvim/repaint_mode.rs index de967e7..70bc4d6 100644 --- a/src/nvim/repaint_mode.rs +++ b/src/nvim/repaint_mode.rs @@ -38,7 +38,6 @@ impl RepaintMode { } } - #[cfg(test)] mod tests { use super::*; diff --git a/src/plug_manager/vim_plug.rs b/src/plug_manager/vim_plug.rs index e30e5b0..3bff9a6 100644 --- a/src/plug_manager/vim_plug.rs +++ b/src/plug_manager/vim_plug.rs @@ -1,6 +1,6 @@ use std::rc::Rc; -use neovim_lib::NeovimApi; +use neovim_lib::{NeovimApi, NeovimApiAsync}; use nvim::{NeovimClient, ErrorReport, NeovimRef}; use value::ValueMapExt; @@ -84,7 +84,9 @@ impl Manager { pub fn reload(&self, path: &str) { if let Some(mut nvim) = self.nvim() { - nvim.command(&format!("source {}", path)).report_err(); + nvim.command_async(&format!("source {}", path)) + .cb(|r| r.report_err()) + .call() } } } diff --git a/src/popup_menu.rs b/src/popup_menu.rs index 544d775..b8c678b 100644 --- a/src/popup_menu.rs +++ b/src/popup_menu.rs @@ -11,9 +11,9 @@ use pango::{self, LayoutExt}; use neovim_lib::{Neovim, NeovimApi}; use color::ColorModel; -use nvim::{self, ErrorReport, CompleteItem}; -use shell; +use nvim::{self, ErrorReport, NeovimClient}; use input; +use render; const MAX_VISIBLE_ROWS: i32 = 10; @@ -32,6 +32,7 @@ struct State { impl State { pub fn new() -> Self { let tree = gtk::TreeView::new(); + tree.get_selection().set_mode(gtk::SelectionMode::Single); let css_provider = gtk::CssProvider::new(); let style_context = tree.get_style_context().unwrap(); @@ -74,27 +75,26 @@ impl State { } } - fn before_show(&mut self, shell: &shell::State, menu_items: &[CompleteItem], selected: i64) { + fn before_show(&mut self, ctx: PopupMenuContext) { if self.nvim.is_none() { - self.nvim = Some(shell.nvim_clone()); + self.nvim = Some(ctx.nvim.clone()); } - let max_width = shell.drawing_area.get_allocated_width(); - self.scroll.set_max_content_width(max_width - 20); + self.scroll.set_max_content_width(ctx.max_width); self.scroll.set_propagate_natural_width(true); - self.update_tree(menu_items, shell); - self.select(selected); + self.update_tree(&ctx); + self.select(ctx.selected); } - fn limit_column_widths(&self, menu: &[CompleteItem], shell: &shell::State) { + fn limit_column_widths(&self, ctx: &PopupMenuContext) { const DEFAULT_PADDING: i32 = 5; - let layout = shell.font_ctx.create_layout(); - let kind_exists = menu.iter().find(|i| i.kind.len() > 0).is_some(); + let layout = ctx.font_ctx.create_layout(); + let kind_exists = ctx.menu_items.iter().find(|i| i.kind.len() > 0).is_some(); let max_width = self.scroll.get_max_content_width(); let (xpad, _) = self.renderer.get_padding(); - let max_word_line = menu.iter().max_by_key(|m| m.word.len()).unwrap(); + let max_word_line = ctx.menu_items.iter().max_by_key(|m| m.word.len()).unwrap(); layout.set_text(max_word_line.word); let (word_max_width, _) = layout.get_pixel_size(); let word_column_width = word_max_width + xpad * 2 + DEFAULT_PADDING; @@ -114,7 +114,7 @@ impl State { } - let max_menu_line = menu.iter().max_by_key(|m| m.menu.len()).unwrap(); + let max_menu_line = ctx.menu_items.iter().max_by_key(|m| m.menu.len()).unwrap(); if max_menu_line.menu.len() > 0 { layout.set_text(max_menu_line.menu); @@ -126,31 +126,28 @@ impl State { } } - fn update_tree(&self, menu: &[CompleteItem], shell: &shell::State) { - if menu.is_empty() { + fn update_tree(&self, ctx: &PopupMenuContext) { + if ctx.menu_items.is_empty() { return; } - self.limit_column_widths(menu, shell); + self.limit_column_widths(ctx); self.renderer.set_property_font( - Some(&shell.get_font_desc().to_string()), + Some(&ctx.font_ctx.font_description().to_string()), ); - let color_model = &shell.color_model; + let color_model = &ctx.color_model; self.renderer.set_property_foreground_rgba( Some(&color_model.pmenu_fg().into()), ); - self.renderer.set_property_background_rgba( - Some(&color_model.pmenu_bg().into()), - ); self.update_css(color_model); let list_store = gtk::ListStore::new(&vec![gtk::Type::String; 4]); let all_column_ids: Vec = (0..4).map(|i| i as u32).collect(); - for line in menu { + for line in ctx.menu_items { let line_array: [&glib::ToValue; 4] = [&line.word, &line.kind, &line.menu, &line.info]; list_store.insert_with_values(None, &all_column_ids, &line_array[..]); } @@ -165,9 +162,11 @@ impl State { match gtk::CssProviderExt::load_from_data( &self.css_provider, &format!( - ".view {{ color: {}; background-color: {};}}", + ".view :selected {{ color: {}; background-color: {};}}\n + .view {{ background-color: {}; }}", fg.to_hex(), - bg.to_hex() + bg.to_hex(), + color_model.pmenu_bg().to_hex(), ).as_bytes(), ) { Err(e) => error!("Can't update css {}", e), @@ -299,30 +298,16 @@ impl PopupMenu { self.open } - pub fn show( - &mut self, - shell: &shell::State, - menu_items: &[CompleteItem], - selected: i64, - x: i32, - y: i32, - width: i32, - height: i32, - ) { - + pub fn show(&mut self, ctx: PopupMenuContext) { self.open = true; self.popover.set_pointing_to(>k::Rectangle { - x, - y, - width, - height, + x: ctx.x, + y: ctx.y, + width: ctx.width, + height: ctx.height, }); - self.state.borrow_mut().before_show( - shell, - menu_items, - selected, - ); + self.state.borrow_mut().before_show(ctx); self.popover.popup() } @@ -339,6 +324,18 @@ impl PopupMenu { } } +pub struct PopupMenuContext<'a> { + pub nvim: &'a Rc, + pub color_model: &'a ColorModel, + pub font_ctx: &'a render::Context, + pub menu_items: &'a [nvim::CompleteItem<'a>], + pub selected: i64, + pub x: i32, + pub y: i32, + pub width: i32, + pub height: i32, + pub max_width: i32, +} fn tree_button_press(tree: >k::TreeView, ev: &EventButton, nvim: &mut Neovim) -> Inhibit { if ev.get_event_type() != EventType::ButtonPress { diff --git a/src/project.rs b/src/project.rs index 8ce293e..a442e11 100644 --- a/src/project.rs +++ b/src/project.rs @@ -59,7 +59,7 @@ pub struct Projects { } impl Projects { - pub fn new(ref_widget: >k::ToolButton, shell: Rc>) -> Rc> { + pub fn new(ref_widget: >k::Button, shell: Rc>) -> Rc> { let projects = Projects { shell, popup: Popover::new(Some(ref_widget)), @@ -73,6 +73,9 @@ impl Projects { projects.setup_tree(); + projects.tree.set_activate_on_single_click(true); + projects.tree.set_hover_selection(true); + projects.tree.set_grid_lines(gtk::TreeViewGridLines::Horizontal); let vbox = gtk::Box::new(Orientation::Vertical, 5); vbox.set_border_width(5); @@ -89,11 +92,12 @@ impl Projects { ); projects.scroll.add(&projects.tree); + projects.scroll.set_shadow_type(gtk::ShadowType::In); vbox.pack_start(&projects.scroll, true, true, 0); let open_btn = gtk::Button::new_with_label("Other Documents…"); - vbox.pack_start(&open_btn, true, true, 0); + vbox.pack_start(&open_btn, true, true, 5); vbox.show_all(); projects.popup.add(&vbox); @@ -129,7 +133,12 @@ impl Projects { let prj_ref = projects.clone(); projects.borrow().tree.connect_row_activated( - move |tree, _, _| { + move |tree, _, column| { + // Don't activate if the user clicked the checkbox. + let toggle_column = tree.get_column(2).unwrap(); + if *column == toggle_column { + return; + } let selection = tree.get_selection(); if let Some((model, iter)) = selection.get_selected() { prj_ref.borrow().open_uri(&model, &iter); @@ -281,6 +290,7 @@ impl Projects { let image_column = TreeViewColumn::new(); let icon_renderer = CellRendererPixbuf::new(); + icon_renderer.set_padding(5, 0); image_column.pack_start(&icon_renderer, true); image_column.add_attribute( @@ -293,18 +303,23 @@ impl Projects { let text_column = TreeViewColumn::new(); - self.name_renderer.set_property_width_chars(60); - self.path_renderer.set_property_width_chars(60); + self.name_renderer.set_property_width_chars(45); + self.path_renderer.set_property_width_chars(45); + self.name_renderer.set_property_ellipsize( + pango::EllipsizeMode::Middle, + ); self.path_renderer.set_property_ellipsize( pango::EllipsizeMode::Start, ); + self.name_renderer.set_padding(0, 5); + self.path_renderer.set_padding(0, 5); text_column.pack_start(&self.name_renderer, true); text_column.pack_start(&self.path_renderer, true); text_column.add_attribute( &self.name_renderer, - "markup", + "text", ProjectViewColumns::Name as i32, ); text_column.add_attribute( @@ -492,7 +507,7 @@ impl Entry { format!("{}", encode_minimal(&s.to_string_lossy())) }) .unwrap_or_else(|| "".to_owned()), - file_name: format!("{}", encode_minimal(name)), + file_name: encode_minimal(name), name: name.to_owned(), pixbuf: BOOKMARKED_PIXBUF, project: true, @@ -513,7 +528,7 @@ impl Entry { format!("{}", encode_minimal(&s.to_string_lossy())) }) .unwrap_or_else(|| "".to_owned()), - file_name: format!("{}", encode_minimal(&name)), + file_name: encode_minimal(&name), name, pixbuf: CURRENT_DIR_PIXBUF, project: true, @@ -534,7 +549,7 @@ impl Entry { format!("{}", encode_minimal(&s.to_string_lossy())) }) .unwrap_or_else(|| "".to_owned()), - file_name: format!("{}", encode_minimal(&name)), + file_name: encode_minimal(&name), name, pixbuf: PLAIN_FILE_PIXBUF, project: false, diff --git a/src/render/context.rs b/src/render/context.rs index 62223c0..bd50c87 100644 --- a/src/render/context.rs +++ b/src/render/context.rs @@ -14,7 +14,9 @@ pub struct Context { impl Context { pub fn new(font_desc: pango::FontDescription) -> Self { - Context { state: ContextState::new(font_desc) } + Context { + state: ContextState::new(font_desc), + } } pub fn update(&mut self, font_desc: pango::FontDescription) { @@ -88,20 +90,18 @@ pub struct CellMetrics { impl CellMetrics { fn new(font_metrics: &pango::FontMetrics) -> Self { - CellMetrics { pango_ascent: font_metrics.get_ascent(), pango_descent: font_metrics.get_descent(), pango_char_width: font_metrics.get_approximate_digit_width(), ascent: font_metrics.get_ascent() as f64 / pango::SCALE as f64, - line_height: (font_metrics.get_ascent() + font_metrics.get_descent()) as f64 / - pango::SCALE as f64, + line_height: (font_metrics.get_ascent() + font_metrics.get_descent()) as f64 + / pango::SCALE as f64, char_width: font_metrics.get_approximate_digit_width() as f64 / pango::SCALE as f64, - underline_position: (font_metrics.get_ascent() - - font_metrics.get_underline_position()) as - f64 / pango::SCALE as f64, - underline_thickness: font_metrics.get_underline_thickness() as f64 / - pango::SCALE as f64, + underline_position: (font_metrics.get_ascent() - font_metrics.get_underline_position()) + as f64 / pango::SCALE as f64, + underline_thickness: font_metrics.get_underline_thickness() as f64 + / pango::SCALE as f64, } } diff --git a/src/render/mod.rs b/src/render/mod.rs index 4019df9..60ce02e 100644 --- a/src/render/mod.rs +++ b/src/render/mod.rs @@ -4,7 +4,7 @@ mod model_clip_iterator; pub use self::context::Context; pub use self::context::CellMetrics; -use self::model_clip_iterator::{RowView, ModelClipIteratorFactory}; +use self::model_clip_iterator::{ModelClipIteratorFactory, RowView}; use mode; use color; @@ -14,24 +14,26 @@ use pango; use cairo; use pangocairo; -use cursor; +use cursor::Cursor; use ui_model; -pub fn render( - ctx: &cairo::Context, - cursor: &cursor::Cursor, - font_ctx: &context::Context, - ui_model: &ui_model::UiModel, - color_model: &color::ColorModel, - mode: &mode::Mode, -) { +pub fn clear(ctx: &cairo::Context, color_model: &color::ColorModel) { ctx.set_source_rgb( color_model.bg_color.0, color_model.bg_color.1, color_model.bg_color.2, ); ctx.paint(); +} +pub fn render( + ctx: &cairo::Context, + cursor: &C, + font_ctx: &context::Context, + ui_model: &ui_model::UiModel, + color_model: &color::ColorModel, + mode: &mode::Mode, +) { let cell_metrics = font_ctx.cell_metrics(); let &CellMetrics { char_width, .. } = cell_metrics; let (cursor_row, cursor_col) = ui_model.get_cursor(); @@ -47,19 +49,19 @@ pub fn render( for cell_view in ui_model.get_clip_iterator(ctx, cell_metrics) { let mut line_x = 0.0; - let RowView { line, row, line_y, .. } = cell_view; + let RowView { + line, row, line_y, .. + } = cell_view; for (col, cell) in line.line.iter().enumerate() { - draw_cell(&cell_view, color_model, cell, col, line_x); draw_underline(&cell_view, color_model, cell, line_x); if row == cursor_row && col == cursor_col { - let double_width = line.line.get(col + 1).map_or( - false, - |c| c.attrs.double_width, - ); + let double_width = line.line + .get(col + 1) + .map_or(false, |c| c.attrs.double_width); ctx.move_to(line_x, line_y); cursor.draw( ctx, @@ -82,19 +84,18 @@ fn draw_underline( cell: &ui_model::Cell, line_x: f64, ) { - if cell.attrs.underline || cell.attrs.undercurl { - let &RowView { ctx, line_y, - cell_metrics: &CellMetrics { - line_height, - char_width, - underline_position, - underline_thickness, - .. - }, + cell_metrics: + &CellMetrics { + line_height, + char_width, + underline_position, + underline_thickness, + .. + }, .. } = cell_view; @@ -129,11 +130,12 @@ fn draw_cell_bg( ctx, line, line_y, - cell_metrics: &CellMetrics { - char_width, - line_height, - .. - }, + cell_metrics: + &CellMetrics { + char_width, + line_height, + .. + }, .. } = cell_view; @@ -157,7 +159,6 @@ fn draw_cell_bg( ctx.fill(); } } - } fn draw_cell( @@ -167,15 +168,11 @@ fn draw_cell( col: usize, line_x: f64, ) { - let &RowView { ctx, line, line_y, - cell_metrics: &CellMetrics { - ascent, - .. - }, + cell_metrics: &CellMetrics { ascent, .. }, .. } = cell_view; @@ -188,7 +185,6 @@ fn draw_cell( show_glyph_string(ctx, item.font(), glyphs); } - } } diff --git a/src/render/model_clip_iterator.rs b/src/render/model_clip_iterator.rs index b849333..452cb1f 100644 --- a/src/render/model_clip_iterator.rs +++ b/src/render/model_clip_iterator.rs @@ -1,4 +1,6 @@ +use std::cmp::min; use std::slice::Iter; + use cairo; use super::context::CellMetrics; @@ -76,17 +78,17 @@ impl ModelClipIteratorFactory for ui_model::UiModel { let model = self.model(); let (x1, y1, x2, y2) = ctx.clip_extents(); - let model_clip = ui_model::ModelRect::from_area(cell_metrics, x1, y1, x2, y2); - let model_clip_top = if model_clip.top <= 0 { + + // in case ctx.translate is used y1 can be less then 0 + // in this case just use 0 as top value + let model_clip = ui_model::ModelRect::from_area(cell_metrics, x1, y1.max(0.0), x2, y2); + + let model_clip_top = if model_clip.top == 0 { 0 } else { model_clip.top - 1 }; - let model_clip_bot = if model_clip.bot >= model.len() - 1 { - model.len() - 1 - } else { - model_clip.bot + 1 - }; + let model_clip_bot = min(model.len() - 1, model_clip.bot + 1); ModelClipIterator { model_idx: model_clip_top, diff --git a/src/shell.rs b/src/shell.rs index 3679a4b..660ed18 100644 --- a/src/shell.rs +++ b/src/shell.rs @@ -1,8 +1,10 @@ +use std; use std::cell::{Cell, RefCell}; use std::rc::Rc; use std::sync::{Arc, Condvar, Mutex}; use std::ops::Deref; use std::thread; +use std::collections::HashMap; use std::time::Duration; use cairo; @@ -19,21 +21,24 @@ use pangocairo; use neovim_lib::{Neovim, NeovimApi, NeovimApiAsync, Value}; use neovim_lib::neovim_api::Tabpage; +use misc::{decode_uri, escape_filename}; use settings::{FontSource, Settings}; use ui_model::{Attrs, ModelRect, UiModel}; use color::{Color, ColorModel, COLOR_BLACK, COLOR_RED, COLOR_WHITE}; -use nvim::{self, CompleteItem, ErrorReport, GuiApi, NeovimClient, NeovimClientAsync, NeovimRef, - RedrawEvents, RepaintMode}; +use nvim::{self, CompleteItem, ErrorReport, NeovimClient, NeovimClientAsync, NeovimRef, + NvimHandler, RepaintMode}; use input; use input::keyval_to_input_string; -use cursor::Cursor; +use cursor::{BlinkCursor, Cursor, CursorRedrawCb}; use ui::UiMutex; -use popup_menu::PopupMenu; +use popup_menu::{self, PopupMenu}; use tabline::Tabline; +use cmd_line::{CmdLine, CmdLineContext}; use error; use mode; use render; use render::CellMetrics; +use subscriptions::{SubscriptionHandle, Subscriptions}; const DEFAULT_FONT_NAME: &str = "DejaVu Sans Mono 12"; pub const MINIMUM_SUPPORTED_NVIM_VERSION: &str = "0.2.2"; @@ -78,18 +83,35 @@ impl Surface { } } +pub struct RenderState { + pub font_ctx: render::Context, + pub color_model: ColorModel, + pub mode: mode::Mode, +} + +impl RenderState { + pub fn new() -> Self { + RenderState { + font_ctx: render::Context::new(FontDescription::from_string(DEFAULT_FONT_NAME)), + color_model: ColorModel::new(), + mode: mode::Mode::new(), + } + } +} + pub struct State { pub model: UiModel, - pub color_model: ColorModel, cur_attrs: Option, mouse_enabled: bool, nvim: Rc, - pub font_ctx: render::Context, - cursor: Option, - popup_menu: RefCell, + cursor: Option>, + popup_menu: PopupMenu, + cmd_line: CmdLine, settings: Rc>, + render_state: Rc>, surface: Option, + enable_double_buffer: bool, resize_request: (i64, i64), resize_timer: Rc>>, @@ -109,26 +131,33 @@ pub struct State { detach_cb: Option>>, nvim_started_cb: Option>>, + command_cb: Option) + Send + 'static>>, + + subscriptions: RefCell, } impl State { pub fn new(settings: Rc>, options: ShellOptions) -> State { let drawing_area = gtk::DrawingArea::new(); - let popup_menu = RefCell::new(PopupMenu::new(&drawing_area)); - let font_ctx = render::Context::new(FontDescription::from_string(DEFAULT_FONT_NAME)); + let render_state = Rc::new(RefCell::new(RenderState::new())); + let popup_menu = PopupMenu::new(&drawing_area); + let cmd_line = CmdLine::new(&drawing_area, render_state.clone()); State { model: UiModel::empty(), - color_model: ColorModel::new(), nvim: Rc::new(NeovimClient::new()), cur_attrs: None, mouse_enabled: true, - font_ctx, cursor: None, popup_menu, + cmd_line, settings, + render_state, surface: None, + enable_double_buffer: std::env::var("NVIM_GTK_DOUBLE_BUFFER") + .map(|opt| opt.trim() == "1") + .unwrap_or(false), resize_request: (-1, -1), resize_timer: Rc::new(Cell::new(None)), @@ -149,10 +178,17 @@ impl State { detach_cb: None, nvim_started_cb: None, + command_cb: None, + + subscriptions: RefCell::new(Subscriptions::new()), } } fn resize_surface(&mut self) { + if !self.enable_double_buffer { + return; + } + if let Some(Surface { width, height, .. }) = self.surface { let alloc = self.drawing_area.get_allocation(); @@ -211,12 +247,22 @@ impl State { } } - pub fn get_font_desc(&self) -> &FontDescription { - self.font_ctx.font_description() + pub fn set_nvim_command_cb(&mut self, cb: Option) + where + F: FnMut(Vec) + Send + 'static, + { + if cb.is_some() { + self.command_cb = Some(Box::new(cb.unwrap())); + } else { + self.command_cb = None; + } } pub fn set_font_desc(&mut self, desc: &str) { - self.font_ctx.update(FontDescription::from_string(desc)); + self.render_state + .borrow_mut() + .font_ctx + .update(FontDescription::from_string(desc)); self.model.clear_glyphs(); self.try_nvim_resize(); self.on_redraw(&RepaintMode::All); @@ -224,13 +270,17 @@ impl State { pub fn open_file(&self, path: &str) { if let Some(mut nvim) = self.nvim() { - nvim.command(&format!("e {}", path)).report_err(); + nvim.command_async(&format!("e {}", path)) + .cb(|r| r.report_err()) + .call(); } } pub fn cd(&self, path: &str) { if let Some(mut nvim) = self.nvim() { - nvim.command(&format!("cd {}", path)).report_err(); + nvim.command_async(&format!("cd {}", path)) + .cb(|r| r.report_err()) + .call(); } } @@ -243,7 +293,7 @@ impl State { } fn close_popup_menu(&self) { - if self.popup_menu.borrow().is_open() { + if self.popup_menu.is_open() { if let Some(mut nvim) = self.nvim() { nvim.input("").report_err(); } @@ -264,18 +314,25 @@ impl State { self.update_dirty_glyphs(); + let render_state = self.render_state.borrow(); + let cell_metrics = render_state.font_ctx.cell_metrics(); + for mut rect in rects { rect.extend_by_items(&self.model); - let (x, y, width, height) = - rect.to_area_extend_ink(&self.model, self.font_ctx.cell_metrics()); + let (x, y, width, height) = rect.to_area_extend_ink(&self.model, cell_metrics); self.drawing_area.queue_draw_area(x, y, width, height); } } #[inline] fn update_dirty_glyphs(&mut self) { - render::shape_dirty(&self.font_ctx, &mut self.model, &self.color_model); + let render_state = self.render_state.borrow(); + render::shape_dirty( + &render_state.font_ctx, + &mut self.model, + &render_state.color_model, + ); } fn im_commit(&self, ch: &str) { @@ -289,7 +346,7 @@ impl State { line_height, char_width, .. - } = self.font_ctx.cell_metrics(); + } = self.render_state.borrow().font_ctx.cell_metrics(); let alloc = self.drawing_area.get_allocation(); ( (alloc.width as f64 / char_width).trunc() as usize, @@ -309,7 +366,7 @@ impl State { let (row, col) = self.model.get_cursor(); let (x, y, width, height) = - ModelRect::point(col, row).to_area(self.font_ctx.cell_metrics()); + ModelRect::point(col, row).to_area(self.render_state.borrow().font_ctx.cell_metrics()); self.im_context.set_cursor_location(&gdk::Rectangle { x, @@ -362,7 +419,10 @@ impl State { fn edit_paste(&self, clipboard: &str) { let nvim = self.nvim(); if let Some(mut nvim) = nvim { - if self.mode.is(&mode::NvimMode::Insert) || self.mode.is(&mode::NvimMode::Normal) { + let render_state = self.render_state.borrow(); + if render_state.mode.is(&mode::NvimMode::Insert) + || render_state.mode.is(&mode::NvimMode::Normal) + { let paste_code = format!("normal! \"{}P", clipboard); nvim.command_async(&paste_code) .cb(|r| r.report_err()) @@ -373,6 +433,50 @@ impl State { }; } } + + fn max_popup_width(&self) -> i32 { + self.drawing_area.get_allocated_width() - 20 + } + + pub fn subscribe(&self, event_name: &str, args: &[&str], cb: F) -> SubscriptionHandle + where + F: Fn(Vec) + 'static, + { + self.subscriptions + .borrow_mut() + .subscribe(event_name, args, cb) + } + + pub fn set_autocmds(&self) { + self.subscriptions + .borrow() + .set_autocmds(&mut self.nvim().unwrap()); + } + + pub fn notify(&self, params: Vec) -> Result<(), String> { + self.subscriptions.borrow().notify(params) + } + + pub fn run_now(&self, handle: &SubscriptionHandle) { + self.subscriptions + .borrow() + .run_now(handle, &mut self.nvim().unwrap()); + } + + pub fn set_font(&mut self, font_desc: String) { + { + let mut settings = self.settings.borrow_mut(); + settings.set_font_source(FontSource::Rpc); + } + + self.set_font_desc(&font_desc); + } + + pub fn on_command(&mut self, args: Vec) { + if let Some(ref mut cb) = self.command_cb { + cb(args); + } + } } pub struct UiState { @@ -392,19 +496,19 @@ impl UiState { #[derive(Clone)] pub struct ShellOptions { nvim_bin_path: Option, - open_path: Option, + open_paths: Vec, timeout: Option, } impl ShellOptions { pub fn new( nvim_bin_path: Option, - open_path: Option, + open_paths: Vec, timeout: Option, ) -> Self { ShellOptions { nvim_bin_path, - open_path, + open_paths, timeout, } } @@ -427,7 +531,7 @@ impl Shell { }; let shell_ref = Arc::downgrade(&shell.state); - shell.state.borrow_mut().cursor = Some(Cursor::new(shell_ref)); + shell.state.borrow_mut().cursor = Some(BlinkCursor::new(shell_ref)); shell } @@ -582,6 +686,29 @@ impl Shell { state.drawing_area.connect_size_allocate(move |_, _| { init_nvim(&ref_state); }); + + let ref_state = self.state.clone(); + let targets = vec![ + gtk::TargetEntry::new("text/uri-list", gtk::TargetFlags::OTHER_APP, 0), + ]; + state + .drawing_area + .drag_dest_set(gtk::DestDefaults::ALL, &targets, gdk::DragAction::COPY); + state + .drawing_area + .connect_drag_data_received(move |_, _, _, _, s, _, _| { + let uris = s.get_uris(); + let command = uris.iter().filter_map(|uri| decode_uri(uri)).fold( + ":ar".to_owned(), + |command, filename| { + let filename = escape_filename(&filename); + command + " " + &filename + }, + ); + let state = ref_state.borrow_mut(); + let mut nvim = state.nvim().unwrap(); + nvim.command_async(&command).cb(|r| r.report_err()).call() + }); } #[cfg(unix)] @@ -619,7 +746,16 @@ impl Shell { let nvim = state.nvim(); if let Some(mut nvim) = nvim { - nvim.command(":wa").report_err(); + nvim.command_async(":wa").cb(|r| r.report_err()).call(); + } + } + + pub fn new_tab(&self) { + let state = self.state.borrow(); + + let nvim = state.nvim(); + if let Some(mut nvim) = nvim { + nvim.command_async(":tabe").cb(|r| r.report_err()).call(); } } @@ -638,6 +774,14 @@ impl Shell { let mut state = self.state.borrow_mut(); state.set_nvim_started_cb(cb); } + + pub fn set_nvim_command_cb(&self, cb: Option) + where + F: FnMut(Vec) + Send + 'static, + { + let mut state = self.state.borrow_mut(); + state.set_nvim_command_cb(cb); + } } impl Deref for Shell { @@ -749,7 +893,7 @@ fn mouse_input(shell: &mut State, input: &str, state: ModifierType, position: (f line_height, char_width, .. - } = shell.font_ctx.cell_metrics(); + } = shell.render_state.borrow().font_ctx.cell_metrics(); let (x, y) = position; let col = (x / char_width).trunc() as u64; let row = (y / line_height).trunc() as u64; @@ -784,32 +928,54 @@ fn gtk_motion_notify(shell: &mut State, ui_state: &mut UiState, ev: &EventMotion Inhibit(false) } +fn gtk_draw_double_buffer(state: &State, ctx: &cairo::Context) { + let (x1, y1, x2, y2) = ctx.clip_extents(); + let surface = state.surface.as_ref().unwrap(); + let buf_ctx = &surface.ctx; + + surface.surface.flush(); + + buf_ctx.save(); + buf_ctx.rectangle(x1, y1, x2 - x1, y2 - y1); + buf_ctx.clip(); + + let render_state = state.render_state.borrow(); + render::clear(buf_ctx, &render_state.color_model); + render::render( + &buf_ctx, + state.cursor.as_ref().unwrap(), + &render_state.font_ctx, + &state.model, + &render_state.color_model, + &render_state.mode, + ); + + ctx.set_source_surface(&surface.surface, 0.0, 0.0); + ctx.paint(); + buf_ctx.restore(); +} + +fn gtk_draw_direct(state: &State, ctx: &cairo::Context) { + let render_state = state.render_state.borrow(); + render::clear(ctx, &render_state.color_model); + render::render( + ctx, + state.cursor.as_ref().unwrap(), + &render_state.font_ctx, + &state.model, + &render_state.color_model, + &render_state.mode, + ); +} + fn gtk_draw(state_arc: &Arc>, ctx: &cairo::Context) -> Inhibit { let state = state_arc.borrow(); if state.nvim.is_initialized() { - - let (x1, y1, x2, y2) = ctx.clip_extents(); - let surface = state.surface.as_ref().unwrap(); - let buf_ctx = &surface.ctx; - - surface.surface.flush(); - - buf_ctx.save(); - buf_ctx.rectangle(x1, y1, x2 - x1, y2 - y1); - buf_ctx.clip(); - - render::render( - &buf_ctx, - state.cursor.as_ref().unwrap(), - &state.font_ctx, - &state.model, - &state.color_model, - &state.mode, - ); - - ctx.set_source_surface(&surface.surface, 0.0, 0.0); - ctx.paint(); - buf_ctx.restore(); + if state.enable_double_buffer { + gtk_draw_double_buffer(&*state, ctx); + } else { + gtk_draw_direct(&*state, ctx); + } } else if state.nvim.is_initializing() { draw_initializing(&*state, ctx); } @@ -846,13 +1012,14 @@ fn show_nvim_init_error(err: &nvim::NvimInitError, state_arc: Arc fn init_nvim_async( state_arc: Arc>, + nvim_handler: NvimHandler, options: ShellOptions, cols: usize, rows: usize, ) { // execute nvim let nvim = match nvim::start( - state_arc.clone(), + nvim_handler, options.nvim_bin_path.as_ref(), options.timeout, ) { @@ -882,9 +1049,7 @@ fn init_nvim_async( }); // attach ui - if let Err(err) = - nvim::post_start_init(nvim, options.open_path.as_ref(), cols as u64, rows as u64) - { + if let Err(err) = nvim::post_start_init(nvim, options.open_paths, cols as u64, rows as u64) { show_nvim_init_error(&err, state_arc.clone()); } else { set_nvim_initialized(state_arc); @@ -934,13 +1099,15 @@ fn set_nvim_initialized(state_arc: Arc>) { } fn draw_initializing(state: &State, ctx: &cairo::Context) { + let render_state = state.render_state.borrow(); + let color_model = &render_state.color_model; + let layout = pangocairo::functions::create_layout(ctx).unwrap(); let alloc = state.drawing_area.get_allocation(); - let layout = state.font_ctx.create_layout(); ctx.set_source_rgb( - state.color_model.bg_color.0, - state.color_model.bg_color.1, - state.color_model.bg_color.2, + color_model.bg_color.0, + color_model.bg_color.1, + color_model.bg_color.2, ); ctx.paint(); @@ -952,9 +1119,9 @@ fn draw_initializing(state: &State, ctx: &cairo::Context) { ctx.move_to(x, y); ctx.set_source_rgb( - state.color_model.fg_color.0, - state.color_model.fg_color.1, - state.color_model.fg_color.2, + color_model.fg_color.0, + color_model.fg_color.1, + color_model.fg_color.2, ); pangocairo::functions::update_layout(ctx, &layout); pangocairo::functions::show_layout(ctx, &layout); @@ -962,11 +1129,11 @@ fn draw_initializing(state: &State, ctx: &cairo::Context) { ctx.move_to(x + width as f64, y); state.cursor.as_ref().unwrap().draw( ctx, - &state.font_ctx, - &state.mode, + &render_state.font_ctx, + &render_state.mode, y, false, - &state.color_model.bg_color, + &color_model.bg_color, ); } @@ -980,32 +1147,38 @@ fn init_nvim(state_ref: &Arc>) { state.model = UiModel::new(rows as u64, cols as u64); let state_arc = state_ref.clone(); + let nvim_handler = NvimHandler::new(state_ref.clone()); let options = state.options.clone(); - thread::spawn(move || init_nvim_async(state_arc, options, cols, rows)); + thread::spawn(move || init_nvim_async(state_arc, nvim_handler, options, cols, rows)); } } -impl RedrawEvents for State { - fn on_cursor_goto(&mut self, row: u64, col: u64) -> RepaintMode { +// Neovim redraw events +impl State { + pub fn on_cursor_goto(&mut self, row: u64, col: u64) -> RepaintMode { let repaint_area = self.model.set_cursor(row as usize, col as usize); self.set_im_location(); RepaintMode::AreaList(repaint_area) } - fn on_put(&mut self, text: &str) -> RepaintMode { - RepaintMode::Area(self.model.put(text, self.cur_attrs.as_ref())) + pub fn on_put(&mut self, text: String) -> RepaintMode { + let ch = text.chars().last().unwrap_or(' '); + let double_width = text.is_empty(); + RepaintMode::Area(self.model.put(ch, double_width, self.cur_attrs.as_ref())) } - fn on_clear(&mut self) -> RepaintMode { + pub fn on_clear(&mut self) -> RepaintMode { + debug!("clear model"); + self.model.clear(); RepaintMode::All } - fn on_eol_clear(&mut self) -> RepaintMode { + pub fn on_eol_clear(&mut self) -> RepaintMode { RepaintMode::Area(self.model.eol_clear()) } - fn on_resize(&mut self, columns: u64, rows: u64) -> RepaintMode { + pub fn on_resize(&mut self, columns: u64, rows: u64) -> RepaintMode { debug!("on_resize {}/{}", columns, rows); if self.model.columns != columns as usize || self.model.rows != rows as usize { @@ -1013,12 +1186,13 @@ impl RedrawEvents for State { } if let Some(mut nvim) = self.nvim.nvim() { - self.color_model.theme.update(&mut *nvim); + let mut render_state = self.render_state.borrow_mut(); + render_state.color_model.theme.update(&mut *nvim); } RepaintMode::Nothing } - fn on_redraw(&mut self, mode: &RepaintMode) { + pub fn on_redraw(&mut self, mode: &RepaintMode) { match *mode { RepaintMode::All => { self.update_dirty_glyphs(); @@ -1030,90 +1204,70 @@ impl RedrawEvents for State { } } - fn on_set_scroll_region(&mut self, top: u64, bot: u64, left: u64, right: u64) -> RepaintMode { + pub fn on_set_scroll_region( + &mut self, + top: u64, + bot: u64, + left: u64, + right: u64, + ) -> RepaintMode { self.model.set_scroll_region(top, bot, left, right); RepaintMode::Nothing } - fn on_scroll(&mut self, count: i64) -> RepaintMode { + pub fn on_scroll(&mut self, count: i64) -> RepaintMode { RepaintMode::Area(self.model.scroll(count)) } - fn on_highlight_set(&mut self, attrs: &[(Value, Value)]) -> RepaintMode { - let mut model_attrs = Attrs::new(); - - for &(ref key_val, ref val) in attrs { - if let Some(key) = key_val.as_str() { - match key { - "foreground" => { - if let Some(fg) = val.as_u64() { - model_attrs.foreground = Some(Color::from_indexed_color(fg)); - } - } - "background" => { - if let Some(bg) = val.as_u64() { - model_attrs.background = Some(Color::from_indexed_color(bg)); - } - } - "special" => { - if let Some(bg) = val.as_u64() { - model_attrs.special = Some(Color::from_indexed_color(bg)); - } - } - "reverse" => model_attrs.reverse = true, - "bold" => model_attrs.bold = true, - "italic" => model_attrs.italic = true, - "underline" => model_attrs.underline = true, - "undercurl" => model_attrs.undercurl = true, - attr_key => println!("unknown attribute {}", attr_key), - }; - } else { - panic!("attr key must be string"); - } - } + pub fn on_highlight_set(&mut self, attrs: HashMap) -> RepaintMode { + let model_attrs = Attrs::from_value_map(&attrs); self.cur_attrs = Some(model_attrs); RepaintMode::Nothing } - fn on_update_bg(&mut self, bg: i64) -> RepaintMode { + pub fn on_update_bg(&mut self, bg: i64) -> RepaintMode { + let mut render_state = self.render_state.borrow_mut(); if bg >= 0 { - self.color_model.bg_color = Color::from_indexed_color(bg as u64); + render_state.color_model.bg_color = Color::from_indexed_color(bg as u64); } else { - self.color_model.bg_color = COLOR_BLACK; + render_state.color_model.bg_color = COLOR_BLACK; } RepaintMode::Nothing } - fn on_update_fg(&mut self, fg: i64) -> RepaintMode { + pub fn on_update_fg(&mut self, fg: i64) -> RepaintMode { + let mut render_state = self.render_state.borrow_mut(); if fg >= 0 { - self.color_model.fg_color = Color::from_indexed_color(fg as u64); + render_state.color_model.fg_color = Color::from_indexed_color(fg as u64); } else { - self.color_model.fg_color = COLOR_WHITE; + render_state.color_model.fg_color = COLOR_WHITE; } RepaintMode::Nothing } - fn on_update_sp(&mut self, sp: i64) -> RepaintMode { + pub fn on_update_sp(&mut self, sp: i64) -> RepaintMode { + let mut render_state = self.render_state.borrow_mut(); if sp >= 0 { - self.color_model.sp_color = Color::from_indexed_color(sp as u64); + render_state.color_model.sp_color = Color::from_indexed_color(sp as u64); } else { - self.color_model.sp_color = COLOR_RED; + render_state.color_model.sp_color = COLOR_RED; } RepaintMode::Nothing } - fn on_mode_change(&mut self, mode: &str, idx: u64) -> RepaintMode { - self.mode.update(mode, idx as usize); + pub fn on_mode_change(&mut self, mode: String, idx: u64) -> RepaintMode { + let mut render_state = self.render_state.borrow_mut(); + render_state.mode.update(&mode, idx as usize); RepaintMode::Area(self.model.cur_point()) } - fn on_mouse(&mut self, on: bool) -> RepaintMode { + pub fn on_mouse(&mut self, on: bool) -> RepaintMode { self.mouse_enabled = on; RepaintMode::Nothing } - fn on_busy(&mut self, busy: bool) -> RepaintMode { + pub fn on_busy(&mut self, busy: bool) -> RepaintMode { if busy { self.cursor.as_mut().unwrap().busy_on(); } else { @@ -1122,7 +1276,7 @@ impl RedrawEvents for State { RepaintMode::Area(self.model.cur_point()) } - fn popupmenu_show( + pub fn popupmenu_show( &mut self, menu: &[CompleteItem], selected: i64, @@ -1130,26 +1284,38 @@ impl RedrawEvents for State { col: u64, ) -> RepaintMode { let point = ModelRect::point(col as usize, row as usize); - let (x, y, width, height) = point.to_area(self.font_ctx.cell_metrics()); + let render_state = self.render_state.borrow(); + let (x, y, width, height) = point.to_area(render_state.font_ctx.cell_metrics()); - self.popup_menu - .borrow_mut() - .show(self, menu, selected, x, y, width, height); + let context = popup_menu::PopupMenuContext { + nvim: &self.nvim, + color_model: &render_state.color_model, + font_ctx: &render_state.font_ctx, + menu_items: &menu, + selected, + x, + y, + width, + height, + max_width: self.max_popup_width(), + }; + + self.popup_menu.show(context); RepaintMode::Nothing } - fn popupmenu_hide(&mut self) -> RepaintMode { - self.popup_menu.borrow_mut().hide(); + pub fn popupmenu_hide(&mut self) -> RepaintMode { + self.popup_menu.hide(); RepaintMode::Nothing } - fn popupmenu_select(&mut self, selected: i64) -> RepaintMode { - self.popup_menu.borrow().select(selected); + pub fn popupmenu_select(&mut self, selected: i64) -> RepaintMode { + self.popup_menu.select(selected); RepaintMode::Nothing } - fn tabline_update( + pub fn tabline_update( &mut self, selected: Tabpage, tabs: Vec<(Tabpage, Option)>, @@ -1159,23 +1325,92 @@ impl RedrawEvents for State { RepaintMode::Nothing } - fn mode_info_set( + pub fn mode_info_set( &mut self, cursor_style_enabled: bool, mode_info: Vec, ) -> RepaintMode { - self.mode.set_info(cursor_style_enabled, mode_info); + let mut render_state = self.render_state.borrow_mut(); + render_state.mode.set_info(cursor_style_enabled, mode_info); + RepaintMode::Nothing + } + + pub fn cmdline_show( + &mut self, + content: Vec<(HashMap, String)>, + pos: u64, + firstc: String, + prompt: String, + indent: u64, + level: u64, + ) -> RepaintMode { + { + let cursor = self.model.cur_point(); + let render_state = self.render_state.borrow(); + let (x, y, width, height) = cursor.to_area(render_state.font_ctx.cell_metrics()); + let ctx = CmdLineContext { + content, + pos, + firstc, + prompt, + indent, + level_idx: level, + x, + y, + width, + height, + max_width: self.max_popup_width(), + }; + + self.cmd_line.show_level(&ctx); + } + + self.on_busy(true) + } + + pub fn cmdline_hide(&mut self, level: u64) -> RepaintMode { + self.cmd_line.hide_level(level); + self.on_busy(false) + } + + pub fn cmdline_block_show( + &mut self, + content: Vec, String)>>, + ) -> RepaintMode { + let max_width = self.max_popup_width(); + self.cmd_line.show_block(&content, max_width); + self.on_busy(true) + } + + pub fn cmdline_block_append( + &mut self, + content: Vec<(HashMap, String)>, + ) -> RepaintMode { + self.cmd_line.block_append(&content); + RepaintMode::Nothing + } + + pub fn cmdline_block_hide(&mut self) -> RepaintMode { + self.cmd_line.block_hide(); + self.on_busy(false) + } + + pub fn cmdline_pos(&mut self, pos: u64, level: u64) -> RepaintMode { + let render_state = self.render_state.borrow(); + self.cmd_line.pos(&*render_state, pos, level); + RepaintMode::Nothing + } + + pub fn cmdline_special_char(&mut self, c: String, shift: bool, level: u64) -> RepaintMode { + let render_state = self.render_state.borrow(); + self.cmd_line.special_char(&*render_state, c, shift, level); RepaintMode::Nothing } } -impl GuiApi for State { - fn set_font(&mut self, font_desc: &str) { - { - let mut settings = self.settings.borrow_mut(); - settings.set_font_source(FontSource::Rpc); - } - - self.set_font_desc(font_desc); +impl CursorRedrawCb for State { + fn queue_redraw_cursor(&mut self) { + let cur_point = self.model.cur_point(); + self.on_redraw(&RepaintMode::Area(cur_point)); } } diff --git a/src/subscriptions.rs b/src/subscriptions.rs new file mode 100644 index 0000000..50db097 --- /dev/null +++ b/src/subscriptions.rs @@ -0,0 +1,156 @@ +use std::collections::HashMap; + +use neovim_lib::{NeovimApi, NeovimApiAsync, Value}; + +use nvim::{ErrorReport, NeovimRef}; + +/// A subscription to a Neovim autocmd event. +struct Subscription { + /// A callback to be executed each time the event triggers. + cb: Box) + 'static>, + /// A list of expressions which will be evaluated when the event triggers. The result is passed + /// to the callback. + args: Vec, +} + +/// A map of all registered subscriptions. +pub struct Subscriptions(HashMap>); + +/// A handle to identify a `Subscription` within the `Subscriptions` map. +/// +/// Can be used to trigger the subscription manually even when the event was not triggered. +/// +/// Could be used in the future to suspend individual subscriptions. +#[derive(Debug)] +pub struct SubscriptionHandle { + event_name: String, + index: usize, +} + +impl Subscriptions { + pub fn new() -> Self { + Subscriptions(HashMap::new()) + } + + /// Subscribe to a Neovim autocmd event. + /// + /// Subscriptions are not active immediately but only after `set_autocmds` is called. At the + /// moment, all calls to `subscribe` must be made before calling `set_autocmds`. + /// + /// This function is wrapped by `shell::State`. + /// + /// # Arguments: + /// + /// - `event_name`: The event to register. + /// See `:help autocmd-events` for a list of supported event names. Event names can be + /// comma-separated. + /// + /// - `args`: A list of expressions to be evaluated when the event triggers. + /// Expressions are evaluated using Vimscript. The results are passed to the callback as a + /// list of Strings. + /// This is especially useful as `Neovim::eval` is synchronous and might block if called from + /// the callback function; so always use the `args` mechanism instead. + /// + /// - `cb`: The callback function. + /// This will be called each time the event triggers or when `run_now` is called. + /// It is passed a vector with the results of the evaluated expressions given with `args`. + /// + /// # Example + /// + /// Call a function each time a buffer is entered or the current working directory is changed. + /// Pass the current buffer name and directory to the callback. + /// ``` + /// let my_subscription = shell.state.borrow() + /// .subscribe("BufEnter,DirChanged", &["expand(@%)", "getcwd()"], move |args| { + /// let filename = &args[0]; + /// let dir = &args[1]; + /// // do stuff + /// }); + /// ``` + pub fn subscribe(&mut self, event_name: &str, args: &[&str], cb: F) -> SubscriptionHandle + where + F: Fn(Vec) + 'static, + { + let entry = self.0.entry(event_name.to_owned()).or_insert(Vec::new()); + let index = entry.len(); + entry.push(Subscription { + cb: Box::new(cb), + args: args.into_iter().map(|&s| s.to_owned()).collect(), + }); + SubscriptionHandle { + event_name: event_name.to_owned(), + index, + } + } + + /// Register all subscriptions with Neovim. + /// + /// This function is wrapped by `shell::State`. + pub fn set_autocmds(&self, nvim: &mut NeovimRef) { + for (event_name, subscriptions) in &self.0 { + for (i, subscription) in subscriptions.iter().enumerate() { + let args = subscription + .args + .iter() + .fold("".to_owned(), |acc, arg| acc + ", " + &arg); + nvim.command_async(&format!( + "au {} * call rpcnotify(1, 'subscription', '{}', {} {})", + event_name, event_name, i, args, + )).cb(|r| r.report_err()) + .call(); + } + } + } + + /// Trigger given event. + fn on_notify(&self, event_name: &str, index: usize, args: Vec) { + if let Some(subscription) = self.0.get(event_name).and_then(|v| v.get(index)) { + (*subscription.cb)(args); + } + } + + /// Wrapper around `on_notify` for easy calling with a `neovim_lib::Handler` implementation. + /// + /// This function is wrapped by `shell::State`. + pub fn notify(&self, params: Vec) -> Result<(), String> { + let mut params_iter = params.into_iter(); + let ev_name = params_iter.next(); + let ev_name = ev_name + .as_ref() + .and_then(Value::as_str) + .ok_or("Error reading event name")?; + let index = params_iter + .next() + .and_then(|i| i.as_u64()) + .ok_or("Error reading index")? as usize; + let args = params_iter + .map(|arg| arg.as_str().map(|s| s.to_owned())) + .collect::>>() + .ok_or("Error reading args")?; + self.on_notify(ev_name, index, args); + Ok(()) + } + + /// Manually trigger the given subscription. + /// + /// The `nvim` instance is needed to evaluate the `args` expressions. + /// + /// This function is wrapped by `shell::State`. + pub fn run_now(&self, handle: &SubscriptionHandle, nvim: &mut NeovimRef) { + let subscription = &self.0.get(&handle.event_name).unwrap()[handle.index]; + let args = subscription + .args + .iter() + .map(|arg| nvim.eval(arg)) + .map(|res| { + res.ok() + .and_then(|val| val.as_str().map(|s: &str| s.to_owned())) + }) + .collect::>>(); + if let Some(args) = args { + self.on_notify(&handle.event_name, handle.index, args); + } else { + error!("Error manually running {:?}", handle); + } + } +} diff --git a/src/sys/pango/item.rs b/src/sys/pango/item.rs index 0237edb..036ecbf 100644 --- a/src/sys/pango/item.rs +++ b/src/sys/pango/item.rs @@ -20,11 +20,6 @@ glib_wrapper! { } impl Item { - #[cfg(test)] - pub fn new() -> Self { - unsafe { from_glib_full(pango_sys::pango_item_new()) } - } - pub fn analysis(&self) -> analysis::Analysis { analysis::Analysis::from(&self.0.analysis) } diff --git a/src/sys/pango/mod.rs b/src/sys/pango/mod.rs index 881bfd9..c0a008d 100644 --- a/src/sys/pango/mod.rs +++ b/src/sys/pango/mod.rs @@ -10,7 +10,6 @@ use std::ptr; use pango; use pango_sys; -use glib_ffi; use glib::translate::*; @@ -23,8 +22,7 @@ pub fn pango_itemize( cached_iter: Option<&mut AttrIterator>, ) -> Vec { unsafe { - //FromGlibPtrContainer::from_glib_full(pango_sys::pango_itemize( - from_glib_full_as_vec(pango_sys::pango_itemize( + FromGlibPtrContainer::from_glib_full(pango_sys::pango_itemize( context.to_glib_none().0, text.as_ptr() as *const i8, start_index as i32, @@ -37,12 +35,6 @@ pub fn pango_itemize( } } - -unsafe fn from_glib_full_as_vec(ptr: *mut glib_ffi::GList) -> Vec { - let num = glib_ffi::g_list_length(ptr) as usize; - FromGlibContainer::from_glib_full_num(ptr, num) -} - pub fn pango_shape( text: &str, offset: usize, diff --git a/src/tabline.rs b/src/tabline.rs index b09fbc7..20f4d57 100644 --- a/src/tabline.rs +++ b/src/tabline.rs @@ -10,7 +10,7 @@ use glib::signal; use pango; -use neovim_lib::NeovimApi; +use neovim_lib::{NeovimApi, NeovimApiAsync}; use neovim_lib::neovim_api::Tabpage; use nvim; @@ -39,6 +39,14 @@ impl State { } } } + + fn close_tab(&self, idx: u32) { + if let Some(mut nvim) = self.nvim.as_ref().unwrap().nvim() { + nvim.command_async(&format!(":tabc {}", idx + 1)) + .cb(|r| r.report_err()) + .call(); + } + } } pub struct Tabline { @@ -55,6 +63,7 @@ impl Tabline { tabs.set_scrollable(true); tabs.set_show_border(false); tabs.set_border_width(0); + tabs.set_hexpand(true); tabs.hide(); let state = Rc::new(RefCell::new(State::new())); @@ -113,7 +122,33 @@ impl Tabline { let title = gtk::Label::new(None); title.set_ellipsize(pango::EllipsizeMode::Middle); title.set_width_chars(25); - self.tabs.append_page(&empty, Some(&title)); + let close_btn = gtk::Button::new_from_icon_name( + "window-close-symbolic", + gtk::IconSize::Menu.into(), + ); + close_btn.set_relief(gtk::ReliefStyle::None); + close_btn.get_style_context().unwrap().add_class("small-button"); + close_btn.set_focus_on_click(false); + let label_box = gtk::Box::new(gtk::Orientation::Horizontal, 0); + label_box.pack_start(&title, true, false, 0); + label_box.pack_start(&close_btn, false, false, 0); + title.show(); + close_btn.show(); + self.tabs.append_page(&empty, Some(&label_box)); + + let tabs = self.tabs.clone(); + let state_ref = Rc::clone(&self.state); + close_btn.connect_clicked(move |btn| { + let current_label = btn + .get_parent().unwrap(); + for i in 0..tabs.get_n_pages() { + let page = tabs.get_nth_page(Some(i)).unwrap(); + let label = tabs.get_tab_label(&page).unwrap(); + if label == current_label { + state_ref.borrow().close_tab(i); + } + } + }); } } else if count > tabs.len() { for _ in tabs.len()..count { @@ -126,6 +161,12 @@ impl Tabline { let tab_label = self.tabs .get_tab_label(&tab_child.unwrap()) .unwrap() + .downcast::() + .unwrap() + .get_children() + .into_iter() + .next() + .unwrap() .downcast::() .unwrap(); tab_label.set_text(tab.1.as_ref().unwrap_or(&"??".to_owned())); diff --git a/src/ui.rs b/src/ui.rs index fd77e4b..14ef1f0 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -1,22 +1,28 @@ use std::cell::{Ref, RefCell, RefMut}; use std::{env, thread}; +use std::path::Path; use std::rc::Rc; use std::sync::Arc; use gdk; use gtk; -use gtk_sys; use gtk::prelude::*; -use gtk::{AboutDialog, ApplicationWindow, HeaderBar, Image, SettingsExt, ToolButton}; +use gtk::{AboutDialog, ApplicationWindow, Button, HeaderBar, Orientation, Paned, SettingsExt}; use gio::prelude::*; use gio::{Menu, MenuExt, MenuItem, SimpleAction}; +use glib::variant::FromVariant; + use toml; +use neovim_lib::Value; + use settings::{Settings, SettingsLoader}; use shell::{self, Shell, ShellOptions}; use shell_dlg; use project::Projects; use plug_manager; +use file_browser::FileBrowserWidget; +use subscriptions::SubscriptionHandle; macro_rules! clone { (@param _) => ( _ ); @@ -37,6 +43,7 @@ macro_rules! clone { const DEFAULT_WIDTH: i32 = 800; const DEFAULT_HEIGHT: i32 = 600; +const DEFAULT_SIDEBAR_WIDTH: i32 = 200; pub struct Ui { initialized: bool, @@ -45,21 +52,30 @@ pub struct Ui { shell: Rc>, projects: Rc>, plug_manager: Arc>, + file_browser: Arc>, } pub struct Components { window: Option, window_state: WindowState, - open_btn: ToolButton, + open_btn: Button, } impl Components { fn new() -> Components { - let save_image = - Image::new_from_icon_name("document-open", gtk_sys::GTK_ICON_SIZE_SMALL_TOOLBAR as i32); - + let open_btn = Button::new(); + let open_btn_box = gtk::Box::new(gtk::Orientation::Horizontal, 3); + open_btn_box.pack_start(>k::Label::new("Open"), false, false, 3); + open_btn_box.pack_start( + >k::Image::new_from_icon_name("pan-down-symbolic", gtk::IconSize::Menu.into()), + false, + false, + 3, + ); + open_btn.add(&open_btn_box); + open_btn.set_can_focus(false); Components { - open_btn: ToolButton::new(Some(&save_image), "Open"), + open_btn, window: None, window_state: WindowState::load(), } @@ -79,6 +95,7 @@ impl Ui { let plug_manager = plug_manager::Manager::new(); let plug_manager = Arc::new(UiMutex::new(plug_manager)); + let file_browser = Arc::new(UiMutex::new(FileBrowserWidget::new())); let comps = Arc::new(UiMutex::new(Components::new())); let settings = Rc::new(RefCell::new(Settings::new())); let shell = Rc::new(RefCell::new(Shell::new(settings.clone(), options))); @@ -93,6 +110,7 @@ impl Ui { settings, projects, plug_manager, + file_browser, } } @@ -106,6 +124,7 @@ impl Ui { settings.init(); let window = ApplicationWindow::new(app); + let main = Paned::new(Orientation::Horizontal); { // initialize window from comps @@ -126,48 +145,6 @@ impl Ui { } } - // Client side decorations including the toolbar are disabled via NVIM_GTK_NO_HEADERBAR=1 - let use_header_bar = env::var("NVIM_GTK_NO_HEADERBAR") - .map(|opt| opt.trim() != "1") - .unwrap_or(true); - - if app.prefers_app_menu() || use_header_bar { - self.create_main_menu(app, &window); - } - - if use_header_bar { - let header_bar = HeaderBar::new(); - - let projects = self.projects.clone(); - header_bar.pack_start(&comps.open_btn); - comps - .open_btn - .connect_clicked(move |_| projects.borrow_mut().show()); - - let save_image = Image::new_from_icon_name( - "document-save", - gtk_sys::GTK_ICON_SIZE_SMALL_TOOLBAR as i32, - ); - let save_btn = ToolButton::new(Some(&save_image), "Save"); - - let shell = self.shell.clone(); - save_btn.connect_clicked(move |_| shell.borrow_mut().edit_save_all()); - header_bar.pack_start(&save_btn); - - let paste_image = Image::new_from_icon_name( - "edit-paste", - gtk_sys::GTK_ICON_SIZE_SMALL_TOOLBAR as i32, - ); - let paste_btn = ToolButton::new(Some(&paste_image), "Paste"); - let shell = self.shell.clone(); - paste_btn.connect_clicked(move |_| shell.borrow_mut().edit_paste()); - header_bar.pack_start(&paste_btn); - - header_bar.set_show_close_button(true); - - window.set_titlebar(Some(&header_bar)); - } - if restore_win_state { if comps.window_state.is_maximized { window.maximize(); @@ -177,15 +154,51 @@ impl Ui { comps.window_state.current_width, comps.window_state.current_height, ); + + main.set_position(comps.window_state.sidebar_width); } else { window.set_default_size(DEFAULT_WIDTH, DEFAULT_HEIGHT); + main.set_position(DEFAULT_SIDEBAR_WIDTH); } } + // Client side decorations including the toolbar are disabled via NVIM_GTK_NO_HEADERBAR=1 + let use_header_bar = env::var("NVIM_GTK_NO_HEADERBAR") + .map(|opt| opt.trim() != "1") + .unwrap_or(true); + + if app.prefers_app_menu() || use_header_bar { + self.create_main_menu(app, &window); + } + + let update_subtitle = if use_header_bar { + Some(self.create_header_bar()) + } else { + None + }; + + let show_sidebar_action = + SimpleAction::new_stateful("show-sidebar", None, &false.to_variant()); + let file_browser_ref = self.file_browser.clone(); let comps_ref = self.comps.clone(); - window.connect_size_allocate(move |window, _| { - gtk_window_size_allocate(window, &mut *comps_ref.borrow_mut()) + show_sidebar_action.connect_change_state(move |action, value| { + if let Some(ref value) = *value { + action.set_state(value); + let is_active = value.get::().unwrap(); + file_browser_ref.borrow().set_visible(is_active); + comps_ref.borrow_mut().window_state.show_sidebar = is_active; + } }); + app.add_action(&show_sidebar_action); + + let comps_ref = self.comps.clone(); + window.connect_size_allocate(clone!(main => move |window, _| { + gtk_window_size_allocate( + window, + &mut *comps_ref.borrow_mut(), + &main, + ); + })); let comps_ref = self.comps.clone(); window.connect_window_state_event(move |_, event| { @@ -199,10 +212,44 @@ impl Ui { }); let shell = self.shell.borrow(); - window.add(&**shell); + let file_browser = self.file_browser.borrow(); + main.pack1(&**file_browser, false, false); + main.pack2(&**shell, true, false); + + window.add(&main); window.show_all(); - window.set_title("NeovimGtk"); + + if restore_win_state { + // Hide sidebar, if it wasn't shown last time. + // Has to be done after show_all(), so it won't be shown again. + let show_sidebar = self.comps.borrow().window_state.show_sidebar; + show_sidebar_action.change_state(&show_sidebar.to_variant()); + } + + let comps_ref = self.comps.clone(); + let update_title = shell.state.borrow().subscribe( + "BufEnter,DirChanged", + &["expand('%:p')", "getcwd()"], + move |args| { + let comps = comps_ref.borrow(); + let window = comps.window.as_ref().unwrap(); + let file_path = &args[0]; + let dir = Path::new(&args[1]); + let filename = if file_path.is_empty() { + "[No Name]" + } else if let Some(rel_path) = Path::new(&file_path) + .strip_prefix(&dir) + .ok() + .and_then(|p| p.to_str()) + { + rel_path + } else { + &file_path + }; + window.set_title(filename); + }, + ); let comps_ref = self.comps.clone(); let shell_ref = self.shell.clone(); @@ -220,12 +267,83 @@ impl Ui { })); let state_ref = self.shell.borrow().state.clone(); + let file_browser_ref = self.file_browser.clone(); let plug_manager_ref = self.plug_manager.clone(); shell.set_nvim_started_cb(Some(move || { + let state = state_ref.borrow(); plug_manager_ref .borrow_mut() .init_nvim_client(state_ref.borrow().nvim_clone()); + file_browser_ref.borrow_mut().init(&state); + state.set_autocmds(); + state.run_now(&update_title); + if let Some(ref update_subtitle) = update_subtitle { + state.run_now(&update_subtitle); + } })); + + let sidebar_action = UiMutex::new(show_sidebar_action); + shell.set_nvim_command_cb(Some(move |args: Vec| { + if let Some(cmd) = args[0].as_str() { + match cmd { + "ToggleSidebar" => { + let action = sidebar_action.borrow(); + let state = !bool::from_variant(&action.get_state().unwrap()).unwrap(); + action.change_state(&state.to_variant()); + } + _ => {} + } + } + })); + } + + fn create_header_bar(&self) -> SubscriptionHandle { + let header_bar = HeaderBar::new(); + let comps = self.comps.borrow(); + let window = comps.window.as_ref().unwrap(); + + let projects = self.projects.clone(); + header_bar.pack_start(&comps.open_btn); + comps + .open_btn + .connect_clicked(move |_| projects.borrow_mut().show()); + + let new_tab_btn = + Button::new_from_icon_name("tab-new-symbolic", gtk::IconSize::SmallToolbar.into()); + let shell_ref = Rc::clone(&self.shell); + new_tab_btn.connect_clicked(move |_| shell_ref.borrow_mut().new_tab()); + new_tab_btn.set_can_focus(false); + new_tab_btn.set_tooltip_text("Open a new tab"); + header_bar.pack_start(&new_tab_btn); + + let paste_btn = + Button::new_from_icon_name("edit-paste-symbolic", gtk::IconSize::SmallToolbar.into()); + let shell = self.shell.clone(); + paste_btn.connect_clicked(move |_| shell.borrow_mut().edit_paste()); + paste_btn.set_can_focus(false); + paste_btn.set_tooltip_text("Paste from clipboard"); + header_bar.pack_end(&paste_btn); + + let save_btn = Button::new_with_label("Save All"); + let shell = self.shell.clone(); + save_btn.connect_clicked(move |_| shell.borrow_mut().edit_save_all()); + save_btn.set_can_focus(false); + header_bar.pack_end(&save_btn); + + header_bar.set_show_close_button(true); + + window.set_titlebar(Some(&header_bar)); + + let shell = self.shell.borrow(); + let update_subtitle = shell.state.borrow().subscribe( + "DirChanged", + &["getcwd()"], + move |args| { + header_bar.set_subtitle(&*args[0]); + }, + ); + + update_subtitle } fn create_main_menu(&self, app: >k::Application, window: >k::ApplicationWindow) { @@ -237,6 +355,10 @@ impl Ui { section.append_item(&MenuItem::new("New Window", "app.new-window")); menu.append_section(None, §ion); + let section = Menu::new(); + section.append_item(&MenuItem::new("Sidebar", "app.show-sidebar")); + menu.append_section(None, §ion); + let section = Menu::new(); section.append_item(&MenuItem::new("Plugins", "app.Plugins")); section.append_item(&MenuItem::new("About", "app.HelpAbout")); @@ -293,12 +415,19 @@ fn gtk_delete(comps: &UiMutex, shell: &RefCell) -> Inhibit { }) } -fn gtk_window_size_allocate(app_window: >k::ApplicationWindow, comps: &mut Components) { +fn gtk_window_size_allocate( + app_window: >k::ApplicationWindow, + comps: &mut Components, + main: &Paned, +) { if !app_window.is_maximized() { let (current_width, current_height) = app_window.get_size(); comps.window_state.current_width = current_width; comps.window_state.current_height = current_height; } + if comps.window_state.show_sidebar { + comps.window_state.sidebar_width = main.get_position(); + } } fn gtk_window_state_event(event: &gdk::EventWindowState, comps: &mut Components) { @@ -312,6 +441,8 @@ struct WindowState { current_width: i32, current_height: i32, is_maximized: bool, + show_sidebar: bool, + sidebar_width: i32, } impl WindowState { @@ -320,6 +451,8 @@ impl WindowState { current_width: DEFAULT_WIDTH, current_height: DEFAULT_HEIGHT, is_maximized: false, + show_sidebar: false, + sidebar_width: DEFAULT_SIDEBAR_WIDTH, } } } @@ -353,6 +486,13 @@ impl UiMutex { } } +impl UiMutex { + pub fn replace(&self, t: T) -> T { + self.assert_ui_thread(); + self.data.replace(t) + } +} + impl UiMutex { pub fn borrow(&self) -> Ref { self.assert_ui_thread(); diff --git a/src/ui_model/cell.rs b/src/ui_model/cell.rs index b2a5a57..b981ed0 100644 --- a/src/ui_model/cell.rs +++ b/src/ui_model/cell.rs @@ -1,4 +1,7 @@ +use std::collections::HashMap; + use color::Color; +use neovim_lib::Value; #[derive(Clone)] pub struct Attrs { @@ -28,6 +31,38 @@ impl Attrs { } } + pub fn from_value_map(attrs: &HashMap) -> Attrs { + let mut model_attrs = Attrs::new(); + + for (ref key, ref val) in attrs { + match key.as_ref() { + "foreground" => { + if let Some(fg) = val.as_u64() { + model_attrs.foreground = Some(Color::from_indexed_color(fg)); + } + } + "background" => { + if let Some(bg) = val.as_u64() { + model_attrs.background = Some(Color::from_indexed_color(bg)); + } + } + "special" => { + if let Some(bg) = val.as_u64() { + model_attrs.special = Some(Color::from_indexed_color(bg)); + } + } + "reverse" => model_attrs.reverse = true, + "bold" => model_attrs.bold = true, + "italic" => model_attrs.italic = true, + "underline" => model_attrs.underline = true, + "undercurl" => model_attrs.undercurl = true, + attr_key => error!("unknown attribute {}", attr_key), + }; + } + + model_attrs + } + fn clear(&mut self) { self.italic = false; self.bold = false; diff --git a/src/ui_model/mod.rs b/src/ui_model/mod.rs index ec71f26..1ac54f9 100644 --- a/src/ui_model/mod.rs +++ b/src/ui_model/mod.rs @@ -2,11 +2,13 @@ mod cell; mod line; mod item; mod model_rect; +mod model_layout; pub use self::cell::{Cell, Attrs}; pub use self::line::{Line, StyledLine}; pub use self::item::Item; pub use self::model_rect::{ModelRect, ModelRectVec}; +pub use self::model_layout::ModelLayout; pub struct UiModel { @@ -91,16 +93,16 @@ impl UiModel { (self.cur_row, self.cur_col) } - pub fn put(&mut self, text: &str, attrs: Option<&Attrs>) -> ModelRect { + pub fn put(&mut self, ch: char, double_width: bool, attrs: Option<&Attrs>) -> ModelRect { let mut changed_region = self.cur_point(); let line = &mut self.model[self.cur_row]; line.dirty_line = true; let cell = &mut line[self.cur_col]; - cell.ch = text.chars().last().unwrap_or(' '); + cell.ch = ch; cell.attrs = attrs.map(Attrs::clone).unwrap_or_else(Attrs::new); - cell.attrs.double_width = text.is_empty(); + cell.attrs.double_width = double_width; cell.dirty = true; self.cur_col += 1; if self.cur_col >= self.columns { @@ -119,6 +121,16 @@ impl UiModel { self.right = right as usize; } + /// Copy rows from 0 to to_row, col from 0 self.columns + /// + /// Don't do any validation! + pub fn copy_rows(&self, target: &mut UiModel, to_row: usize) { + for (row_idx, line) in self.model[0..to_row + 1].iter().enumerate() { + let mut target_row = &mut target.model[row_idx]; + line.copy_to(target_row, 0, self.columns - 1); + } + } + #[inline] fn copy_row(&mut self, target_row: i64, offset: i64, left_col: usize, right_col: usize) { debug_assert_ne!(0, offset); @@ -293,7 +305,7 @@ mod tests { model.set_cursor(1, 1); - let rect = model.put(" ", None); + let rect = model.put(' ', false, None); assert_eq!(1, rect.top); assert_eq!(1, rect.left); diff --git a/src/ui_model/model_layout.rs b/src/ui_model/model_layout.rs new file mode 100644 index 0000000..0ff40b4 --- /dev/null +++ b/src/ui_model/model_layout.rs @@ -0,0 +1,251 @@ +use std::cmp::max; + +use unicode_width::UnicodeWidthChar; + +use ui_model::{Attrs, UiModel}; + +pub struct ModelLayout { + pub model: UiModel, + rows_filled: usize, + cols_filled: usize, + lines: Vec, Vec)>>, +} + +impl ModelLayout { + const ROWS_STEP: usize = 10; + + pub fn new(columns: u64) -> Self { + ModelLayout { + model: UiModel::new(ModelLayout::ROWS_STEP as u64, columns), + rows_filled: 0, + cols_filled: 0, + lines: Vec::new(), + } + } + + pub fn layout_append(&mut self, mut lines: Vec, Vec)>>) { + let rows_filled = self.rows_filled; + let take_from = self.lines.len(); + + self.lines.append(&mut lines); + + self.layout_replace(rows_filled, take_from); + } + + pub fn layout(&mut self, lines: Vec, Vec)>>) { + self.lines = lines; + self.layout_replace(0, 0); + } + + pub fn set_cursor(&mut self, col: usize) { + let row = if self.rows_filled > 0 { + self.rows_filled - 1 + } else { + 0 + }; + + self.model.set_cursor(row, col); + } + + pub fn size(&self) -> (usize, usize) { + ( + max(self.cols_filled, self.model.get_cursor().1 + 1), + self.rows_filled, + ) + } + + fn check_model_size(&mut self, rows: usize) { + if rows > self.model.rows { + let model_cols = self.model.columns; + let model_rows = ((rows / (ModelLayout::ROWS_STEP + 1)) + 1) * ModelLayout::ROWS_STEP; + let (cur_row, cur_col) = self.model.get_cursor(); + + let mut model = UiModel::new(model_rows as u64, model_cols as u64); + self.model.copy_rows(&mut model, self.rows_filled - 1); + model.set_cursor(cur_row, cur_col); + self.model = model; + } + } + + pub fn insert_char(&mut self, c: &str, shift: bool) { + if c.is_empty() { + return; + } + + let ch = c.chars().next().unwrap(); + let (row, col) = self.model.get_cursor(); + + if shift { + self.insert_into_lines(ch); + self.layout_replace(0, 0); + } else { + self.model.put(ch, false, None); + } + + self.model.set_cursor(row, col); + } + + fn insert_into_lines(&mut self, ch: char) { + let line = &mut self.lines[0]; + + let cur_col = self.model.cur_col; + + let mut col_idx = 0; + for &mut (_, ref mut chars) in line { + if cur_col < col_idx + chars.len() { + let col_sub_idx = cur_col - col_idx; + chars.insert(col_sub_idx, ch); + } else { + col_idx += chars.len(); + } + } + } + + /// Wrap all lines into model + /// + /// returns actual width + fn layout_replace(&mut self, row_offset: usize, take_from: usize) { + let rows = ModelLayout::count_lines(&self.lines[take_from..], self.model.columns); + + self.check_model_size(rows + row_offset); + self.rows_filled = rows + row_offset; + + let lines = &self.lines[take_from..]; + + let mut max_col_idx = 0; + let mut col_idx = 0; + let mut row_idx = row_offset; + for content in lines { + for &(ref attr, ref ch_list) in content { + for ch in ch_list { + let ch_width = ch.width().unwrap_or(1); + + if col_idx + ch_width > self.model.columns { + col_idx = 0; + row_idx += 1; + } + + self.model.set_cursor(row_idx, col_idx as usize); + self.model.put(*ch, false, attr.as_ref()); + if ch_width > 1 { + self.model.put(' ', true, attr.as_ref()); + } + + if max_col_idx < col_idx { + max_col_idx = col_idx + ch_width - 1; + } + + col_idx += ch_width; + } + + if col_idx < self.model.columns { + self.model.model[row_idx].clear(col_idx, self.model.columns - 1); + } + } + row_idx += 1; + } + + if self.rows_filled == 1 { + self.cols_filled = max_col_idx + 1; + } else { + self.cols_filled = max(self.cols_filled, max_col_idx + 1); + } + } + + fn count_lines(lines: &[Vec<(Option, Vec)>], max_columns: usize) -> usize { + let mut row_count = 0; + + for line in lines { + let len: usize = line.iter().map(|c| c.1.len()).sum(); + row_count += len / (max_columns + 1) + 1; + } + + row_count + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_count_lines() { + let lines = vec![vec![(None, vec!['a'; 5])]]; + + let rows = ModelLayout::count_lines(&lines, 4); + assert_eq!(2, rows); + } + + #[test] + fn test_resize() { + let lines = vec![vec![(None, vec!['a'; 5])]; ModelLayout::ROWS_STEP]; + let mut model = ModelLayout::new(5); + + model.layout(lines.clone()); + let (cols, rows) = model.size(); + assert_eq!(5, cols); + assert_eq!(ModelLayout::ROWS_STEP, rows); + + model.layout_append(lines); + let (cols, rows) = model.size(); + assert_eq!(5, cols); + assert_eq!(ModelLayout::ROWS_STEP * 2, rows); + assert_eq!(ModelLayout::ROWS_STEP * 2, model.model.rows); + } + + #[test] + fn test_cols_filled() { + let lines = vec![vec![(None, vec!['a'; 3])]; 1]; + let mut model = ModelLayout::new(5); + + model.layout(lines); + let (cols, _) = model.size(); + assert_eq!(4, cols); // size is 3 and 4 - is with cursor position + + let lines = vec![vec![(None, vec!['a'; 2])]; 1]; + + model.layout_append(lines); + let (cols, _) = model.size(); + assert_eq!(3, cols); + } + + #[test] + fn test_insert_shift() { + let lines = vec![vec![(None, vec!['a'; 3])]; 1]; + let mut model = ModelLayout::new(5); + model.layout(lines); + model.set_cursor(1); + + model.insert_char("b", true); + + let (cols, _) = model.size(); + assert_eq!(4, cols); + assert_eq!('b', model.model.model()[0].line[1].ch); + } + + #[test] + fn test_insert_no_shift() { + let lines = vec![vec![(None, vec!['a'; 3])]; 1]; + let mut model = ModelLayout::new(5); + model.layout(lines); + model.set_cursor(1); + + model.insert_char("b", false); + + let (cols, _) = model.size(); + assert_eq!(3, cols); + assert_eq!('b', model.model.model()[0].line[1].ch); + } + + #[test] + fn test_double_width() { + let lines = vec![vec![(None, vec!['あ'; 3])]; 1]; + let mut model = ModelLayout::new(7); + model.layout(lines); + model.set_cursor(1); + + let (cols, rows) = model.size(); + assert_eq!(1, rows); + assert_eq!(6, cols); + } +} diff --git a/src/ui_model/model_rect.rs b/src/ui_model/model_rect.rs index 8fad371..9c7873e 100644 --- a/src/ui_model/model_rect.rs +++ b/src/ui_model/model_rect.rs @@ -56,8 +56,8 @@ pub struct ModelRect { impl ModelRect { pub fn new(top: usize, bot: usize, left: usize, right: usize) -> ModelRect { - debug_assert!(top <= bot); - debug_assert!(left <= right); + debug_assert!(top <= bot, "{} <= {}", top, bot); + debug_assert!(left <= right, "{} <= {}", left, right); ModelRect { top: top,